LeadRefinery — How the Platform Works

D3 — intent

Traced at commit e53d0dc · libs/contracts/src/lib/{vector-resolution,extractor,canonicalization,contracts,outbox}.ts · libs/temporal/src/{workflows,activities}/call-attempt.ts · apps/worker/src/main.ts · live state read 10 September 2026

Summary

  1. D3 records what the person intends. It holds one value or nothing.
  2. Five values exist. The model can also answer UNKNOWN, which the platform discards.
  3. Two values give not_qualified. Three give qualified_no_action.
  4. D3 is the last step of the headline waterfall. Every other dimension outranks it.
  5. The voice provider can set one value only: INTERESTED. It sets D4 QUALIFIED at the same time.
  6. That rule needs a field named qualified. No live agent declares it.
  7. The phrase list and the model run on every answered call.
  8. Step 1 never sets D3 on a real call, so steps 2 and 3 decide every real D3 value.
  9. The phrase list checks values in a fixed order. Any refusal phrase beats any interest phrase.
  10. In a test, a caller who answered a greeting with I'm good became NOT_INTERESTED.
  11. The outbox reads D3 directly. NOT_INTERESTED can send the client a not_interested email.

Workflow 1 — where the value comes from

D3 has three sources, and they run in a fixed order. Step 1, the voice provider’s analysis, can set one value only: INTERESTED. It needs call successful to be true and the custom field named qualified to be true, and the same rule sets D4 to QUALIFIED, so from step 1 the two values always arrive together. A value from step 1 ends the flow for D3. If D3 is still empty, step 2, a phrase list of 55 phrases, joins the agent’s and the caller’s turns into one string and checks the five values in a fixed order. The first value with a match wins and discards the model answer. Only when no phrase matched does step 3, the language model, fill D3, and an UNKNOWN answer never fills it. The model request goes out before the phrase list runs, so a phrase match discards a paid answer. Steps 2 and 3 both record the source as llm. No live agent declares the field named qualified, so steps 2 and 3 decide every real D3 value.
Workflow 1 of 3 — the sources. Step 1 can set one value, and only together with D4.

Step 1 — the voice provider

  1. The platform reads call_successful from the analysis. It reads qualified from the custom analysis data.
  2. When both are the boolean true, step 1 sets D3 INTERESTED.
  3. The same rule sets D4 QUALIFIED. From step 1, the two values always arrive together.
  4. When call_successful is true and qualified is not true, step 1 sets only D4.
  5. Step 1 cannot set NOT_INTERESTED, CALLBACK_REQUESTED, MORE_INFO_NEEDED or OBJECTION.
  6. No live agent declares qualified, so step 1 never sets D3 on a real call.
  7. Steps 2 and 3 therefore decide every real D3 value.
  8. See D4 — qualification for the effect of that missing field on the headline.

Steps 2 and 3

  1. The phrase list and the model work as they do for D2. See D2 — who answered.
  2. The D3 list holds 55 phrases. Their confidence runs from 0.85 to 0.95.
  3. Step 2 checks the values in this order: NOT_INTERESTED, INTERESTED, CALLBACK_REQUESTED, MORE_INFO_NEEDED, OBJECTION.
  4. A refusal phrase anywhere in the call therefore beats an interest phrase anywhere in the call.
  5. Step 2 reads the agent’s words too. In a test, an agent line with Sounds good. set INTERESTED.
  6. Step 3 fills D3 only when no phrase matched. The platform discards an UNKNOWN answer.

Workflow 2 — what each value does

A table of the D3 values in the order the phrase list checks them. Row 1, NOT_INTERESTED, declined the offer: 12 phrases at 0.90, never set by step 1, headline not qualified. Row 2, INTERESTED, wants to hear more: 14 phrases at 0.90, the only value step 1 can set, and only together with D4 QUALIFIED, headline qualified no action. Row 3, CALLBACK_REQUESTED, asked for a call at another time: 11 phrases at 0.95, never set by step 1, headline qualified no action. Row 4, MORE_INFO_NEEDED, wants details or time to decide: 9 phrases at 0.85, headline qualified no action. Row 5, OBJECTION, raised cost, trust or timing: 9 phrases at 0.85, headline not qualified. UNKNOWN comes only from the model and is never stored. D3 is the last step of the waterfall, so any QUALIFIED or DISQUALIFIED value in D4 outranks it. Three values share one headline, and the outbox reads NOT_INTERESTED directly.
Workflow 2 of 3 — the values. Five values in the order that step 2 checks them. Three share one headline.
  1. D3 decides the headline only when D5, D6, D1, D2 and D4 all give no headline.
  2. A D3 value from steps 2 and 3 reaches the headline only when step 1 left it at needs_review.
  3. NOT_INTERESTED and OBJECTION give not_qualified.
  4. INTERESTED, CALLBACK_REQUESTED and MORE_INFO_NEEDED give qualified_no_action.
  5. The headline has no callback value. A scheduled callback comes from D6 or from a callback tool call.

The outbox

  1. The outbox reads D3 after the headline and the tool calls.
  2. A stop request, a booking, a transfer, or a callback (booked or ASAP) comes first.
  3. If none of those applies, NOT_INTERESTED gives the outbox outcome not_interested.
  4. The outbox ignores the other four D3 values.
  5. An outbox outcome makes the workflow run notifyOutbox, which emails the client’s CRM.

Workflow 3 — one greeting

One greeting read by step 2. The agent says: How are you doing today? The caller says: I am good. Yes, tell me more. Step 2 joins both turns into one string and drops the speaker labels. It checks NOT_INTERESTED first and finds the phrase i’m good, which here only answers a greeting, so it stops. INTERESTED is never checked, although tell me more would have matched. The record stores NOT_INTERESTED at confidence 0.9 with the source llm, and the model’s answer, INTERESTED, leaves no trace. If D4 is empty the headline becomes not qualified, and the outbox outcome becomes not interested. A second test, No thanks. Actually, wait, tell me more, also returns NOT_INTERESTED, because step 2 never reads word order. Measured on 10 September 2026 with the real extractor.
Workflow 3 of 3 — one greeting. A refusal phrase beats an interest phrase, wherever each one occurs.
  1. The test ran the real extractor on the two turns in the diagram.
  2. A model stub answered INTERESTED with confidence 0.9.
  3. The extractor returned NOT_INTERESTED at 0.9, from the phrase i'm good.
  4. The caller used the phrase to answer a greeting. The caller then asked to hear more.
  5. Step 2 checked NOT_INTERESTED first and stopped. It never reached tell me more.
  6. The merged result had the headline not_qualified, confidence 0.9 and no review.
  7. A real call with that greeting gets the same result.
  8. The table below shows more lines from the same tests.
TranscriptStep 2 resultMatched phrase
No thanks. Actually, wait, tell me more.NOT_INTERESTEDno thanks
We already have a line of credit, but I'm interested.NOT_INTERESTEDalready have
We're good on cash flow for now, but tell me more.NOT_INTERESTEDwe're good
I'll pass that on to my husband.NOT_INTERESTEDpass
Agent: Sounds good. Caller: I need to talk to my partner first.INTERESTEDsounds good
What else do you need from me?INTERESTEDwhat else

The phrase list

Step 2 checks the rows from top to bottom. The first row with a match wins. Dictionary version 1.0.

OrderValueConfidencePhrases
1NOT_INTERESTED0.90not interested · no thank you · no thanks · don't need it · don't want it · pass · i'll pass · not for me · we're good · i'm good · already have · not looking
2INTERESTED0.90i'm interested · i am interested · im interested · tell me more · sounds good · i want to know more · yes please · that's interesting · go ahead · i'd like to hear · keep talking · what else · i'm listening · sounds promising
3CALLBACK_REQUESTED0.95call me back · call back later · try again later · not a good time · bad time · busy right now · i'm driving · i'm in a meeting · call tomorrow · call next week · can you call later
4MORE_INFO_NEEDED0.85send me information · email me · send me details · need to think about it · let me think · need to discuss · talk to my spouse · talk to my partner · need more time
5OBJECTION0.85too expensive · can't afford · not in the budget · don't trust · sounds like a scam · is this a scam · don't believe · not falling for · heard this before

Known defects

  1. Value order beats word order. Any refusal phrase beats any interest phrase, wherever each one occurs.
  2. Step 2 reads the agent. An agent line with sounds good or go ahead sets INTERESTED.
  3. Short phrases match ordinary speech. pass, i'm good, we're good, already have and what else occur in calls with other intents.
  4. Step 1 ties D3 to D4. Step 1 sets INTERESTED only with QUALIFIED. An interested person who does not qualify gets no D3 value.
  5. Three values share one headline. The headline cannot separate a callback request from interest.
  6. A false refusal reaches the client. A wrong NOT_INTERESTED can send a not_interested email to the client’s CRM.
  7. The outbox ignores the headline for D3. A wrong_number call with NOT_INTERESTED still gives not_interested. The outbox comment says a CRM should not hear about a wrong number.
  8. One label for two sources. Step 2 and step 3 both write llm.

What rehearsal data can show

  1. A sample of about 1% of stage attempts held 338 D3 values.
  2. The phrase list set 244 of them. The phrase go ahead set 203.
  3. The simulator’s caller line Yeah, go ahead. produces those matches.
  4. The impersonated model set 63. Step 1 set the other 31.
  5. A rehearsal therefore tests the phrase list against a script. It does not measure a real caller.

D2 — who answered — the gate, and the phrase list reading the agent.
D4 — qualification — the value that outranks D3.
Outcome resolution — how D3 fits among the seven dimensions.