Appearance
Lead qualification
Qualification of an inbound demo request, decided by rules, a model and a sales development rep together in one durable workflow: the lead answers a short form, rules check the answers sales needs and the size of the team before any model is asked, a model reads the request for fit, buying intent and segment, and a rep takes every lead that is not plainly ready for sales. Every answer, rule result and decision stays on the record. Pack: examples/policies/lead-qualification/.
A pattern for a lead qualification decision, not sales advice: the team-size bands, the customer profile in the model's questions and the fields are placeholders chosen to make the example run; a real policy comes from the business's own rules. Its model step calls an install named chatgpt (The chatgpt install).
The decision it automates
Whether to pass the lead to sales (approve), send it to a sales development rep (manual_review), or put it in nurture (reject), when the demo request is sent. The rules put a team under five in nurture on their own. A lead goes to sales without a person only when every rule passes and the model reads it as a fit, at 0.8 or more, with buying intent at evaluating or above. The model never puts a lead in nurture: one it reads as a poor fit goes to a rep at low priority, who decides. Both outcomes route a lead rather than judge it: sales can hand a lead back, and a lead in nurture that asks again is qualified again.
Data expected
- The lead as a subject (kind
user) withnameandemail, created by your website or your CRM before it starts the run. - The form's answers under
submission:fullName,workEmail,company,teamSize,useCase, and the optionalphone,roleandtimeline.
Steps and rules
| Step | Type | Why |
|---|---|---|
collect | wait_for_collection | The demo-request flow: who is asking, then the team, the use case and the timeline. |
rules | evaluate_rules | The lead-qualification rule set under max_severity. No rule reads the model's answers, so the rules run first. |
gate | branch | rules.outcome == reject goes straight to the decision: a lead the rules turn away costs no model call. |
qualify | call_app | chatgpt's decide: fit, intent and segment, kept under ai. |
route | branch | rules.outcome == manual_review opens a case; ai.answers.qualified.probability of 0.8 or more goes on to intent, 0.2 or less to poor_fit, anything between to a case. |
intent | branch | ai.answers.intent.value of 2 or more (evaluating, or ready to buy) goes to the decision; less opens a case. |
review | create_case | sales_development, priority high, 4-hour SLA: an inbound lead cools in hours. Its routing label is the segment the model chose (queueFrom: ai.answers.segment.value). |
poor_fit | create_case | sales_development, priority low, 72-hour SLA, with the same label: a lead the model reads as a poor fit, which a rep puts in nurture or passes on. |
decide | emit_decision | From the rules, or the rep's decision. |
The rule set lead-qualification (useCase: custom) uses max_severity: a failed block rule rejects, a failed warn rule sends the run to review, and the weights add up to the score.
| Rule | Type | Severity, weight | Fails when |
|---|---|---|---|
lead_team_size_minimum | score threshold | block, 100 | teamSize is under 5 |
lead_required_answers | expression | warn, 30 | role or timeline is missing |
lead_team_size_enterprise | score threshold | warn, 30 | teamSize is above 1,000 |
The form leaves the role and the timeline optional, so that a lead who does not know them still asks; sales plans the first call from them, so a lead without them goes to a rep. A team over 1,000 goes to a rep whatever the model reads: enterprise deals are routed by hand.
The model step
qualify asks three questions about one state, the lead's company, role, teamSize, useCase and timeline, which its input mapping puts in the step's variables (Asking for decisions):
| Question | Answer | Asks | Read by |
|---|---|---|---|
qualified | yes or no, threshold 0.8 | Is the request from a business that fits the customer profile in its criteria, with a need a conversation with sales can move forward? | route, by its probability |
intent | a score on four levels | How strong is the buying intent: none, early, evaluating or ready? | intent, by its level |
segment | smb, mid_market or enterprise | Which segment is the company in? | the cases' routing label |
The state is the variables themselves, not a template: a lead who leaves the role or the timeline empty is still read, where a template naming them would fail the step. A probability between 0.2 and 0.8, a model that cannot tell included, takes the branch's default to a rep (Routing on an answer).
What the vendor sees
Through the install's connection, the step's questions and the five answers above: not the lead's name, email or phone (Data protection). The use case is the lead's own text, and a lead can write anything in it. The model reads it as data, and the most a lead wins by talking it round is a sales call that every rule allowed (Prompt injection).
The chatgpt install
A pack never carries an install: this one declares the install its step calls, "installs": { "chatgpt": "chatgpt" }, an install of the platform's ChatGPT (OpenAI API) app. The import reports chatgpt missing until the workspace has one (--publish publishes all the same), and a run that reaches qualify without it fails with unknown_install. The admin console's import result installs it in one click, with a preset configuration that reaches OpenAI's Responses API with storing off, for decisions too, and a sample key (Installs at import and export).
OpenAI refuses the sample key. Until an administrator stores the workspace's own OpenAI key on the install's page (Connect › Apps, the install chatgpt, Configuration › Keys › openaiApiKey), every run that reaches qualify fails there: it gets no decision and opens no case, and OpenAI's answer shows in the invocation record and under the run's app calls. The same holds whenever the call fails for good, through an outage that outlasts its retries, an exhausted quota or a refusal (Time, retries and failures). A lead the rules put in nurture never reaches the model, so its run completes. Once the key is stored, replay a failed run from qualify (Operate › Runs, Replay from step: Admin console: Runs): the lead is not asked again. Every model app takes the same input and answers in the same shape, so the step can call an install of claude, gemini, jev or openai-compatible instead, declared under its own name, or fall back on one (Falling back to another install).
What a reviewer sees
A sales_development case labeled with the segment the model chose, at high priority for a lead the rules or an unsure model referred and low for a poor fit, with the lead's answers, the rule results, the model's three answers and its call under Vendor checks. The rep's approve passes the lead to sales and reject puts it in nurture. Request info sends the form back, and the run checks the new answers against the rules and asks the model again before the case returns. decision.created tells your CRM of every decision, the rep's or the run's own (Outbound webhooks): it carries the run's id, and the run holds the segment and the intent the CRM assigns the lead by.
How to adapt it
Write your own customer profile into the criteria of qualified and your own segments into segment, and move the bands and the route's 0.8 and 0.2 to your own.
To enrich the request from your CRM without making the pack depend on one, install the platform's http-api once for your CRM's API, named crm (App catalog), and add a step between collect and rules that looks the company up:
json
{
"id": "enrich",
"type": "call_app",
"app": "crm",
"action": "request",
"input": {
"endpoint": "/companies/search",
"params": [{ "key": "email", "value": "<<email>>" }],
"acceptStatuses": [404],
"pick": ["company.employees", "company.industry", "company.stage"]
},
"inputMapping": { "variables.email": "submission.workEmail" },
"outputKey": "crm",
"next": "rules"
}"variables.crm": "crm.body.company" in the input mapping of qualify then lets the model read the CRM's record beside the lead's answers; a company the CRM does not know answers 404, an answer by acceptStatuses, and the model reads the request without it. A rule can read the record too, such as an expression that sends an existing customer to a rep (getPath("crm.body.company.stage") == "customer"). The pack then declares the install it calls, "installs": { "chatgpt": "chatgpt", "crm": "http-api" } (Adding a vendor step).
Run it
bash
pnpm policy:import examples/policies/lead-qualification --publishpnpm policies:check runs two fixtures against a model stub (scripts/ai-stub.mjs), whose answers a cue word in the use case picks:
expect.json: a demo for an operations team of 40 at a freight company starting this quarter, cued high. The three rules pass, the stub answersqualifiedat 0.95 andintentat its top level, and the run completes with an automatedapprove, without a case.expect-poor-fit.json: a graduate researcher who wants a demo for a thesis, cued low. The rules pass, the stub answersqualifiedat 0.05, and a low-prioritysales_developmentcase labeledsmbopens; the check puts the lead in nurture and the run completes with a manualreject.