LeadRefinery — How the Platform Works

D2 — who answered

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. D2 records who answered the call. It holds one value or nothing.
  2. Four values exist. The model can also answer UNKNOWN, which the platform discards.
  3. Only WRONG_PERSON and REASSIGNED change the headline. Both give wrong_number.
  4. Three sources can fill D2. They apply in a fixed order, and the first value stays.
  5. The voice provider runs first. It can set WRONG_PERSON and no other value.
  6. A phrase list runs second. A language model runs third.
  7. The phrase list and the model run on every answered call.
  8. On a real call, D2 can therefore hold any of the four values.
  9. The phrase list reads the agent’s words as well as the caller’s words.
  10. In a test, the agent question Am I speaking with Sam? turned a wrong number into DECISION_MAKER.
  11. In a second test, a shop owner who said retired became REASSIGNED and wrong_number.

Workflow 1 — where the value comes from

D2 has three sources, and they run in a fixed order. Step 1, the voice provider’s analysis, can set one value only: WRONG_PERSON. It needs call successful to be false, in voicemail to be false, and the words wrong number or wrong person in the call summary. A value from step 1 ends the flow for D2. If D2 is still empty, step 2, a phrase list of 35 phrases, joins the agent’s and the caller’s turns into one string and checks the four 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 D2, 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, and the record keeps only the winning value. The value lands in the disposition vector under d2Party, and a value from step 2 or 3 changes the headline only if step 1 left the headline at needs review.
Workflow 1 of 3 — the sources. Three sources in a fixed order. All three run on every answered call.

Step 1 — the voice provider

  1. The provider sends a call analysis after the call ends.
  2. The platform reads three fields from it: call_successful, in_voicemail and call_summary.
  3. The platform sets WRONG_PERSON only when all three conditions below hold.
  4. call_successful is the boolean false.
  5. in_voicemail is the boolean false. An absent field does not satisfy this condition.
  6. call_summary contains wrong number or wrong person, in any letter case.
  7. In a test, an analysis without in_voicemail gave no D2 value. The headline stayed needs_review.
  8. The platform stores the value with confidence 0.8 and the source retell_analysis.
  9. Step 1 cannot set DECISION_MAKER, GATEKEEPER or REASSIGNED.

Before step 2

  1. The workflow asks for extraction only for an answered call.
  2. Each answered call makes a model request. The model is gpt-4o-mini unless OPENAI_MODEL names another.
  3. Before the request, the extractor removes phone numbers, email addresses and card numbers from the transcript.
  4. It also removes social security numbers, street addresses and ZIP codes. It matches fixed patterns only.
  5. It does not remove names. It sends the provider’s call summary without any removal.
  6. In a test, a phone number with spaces and the whole call summary reached the model unchanged.
  7. The extractor sends the model request before it checks the phrase list. A phrase match then discards the paid answer.
  8. If the request fails, the extractor uses the phrase list alone.
  9. The cost Blueprint does not price this request.
  10. A rehearsal runs extraction against an impersonated model and spends no tokens.
  11. Extraction needs OPENAI_API_KEY. Without it, steps 2 and 3 do not run.

Step 2 — the phrase list

  1. Step 2 outranks the model. A phrase match wins even when the model answered first.
  2. It joins every turn of the transcript into one string. It drops the speaker labels.
  3. It checks the values in a fixed order: DECISION_MAKER, GATEKEEPER, WRONG_PERSON, REASSIGNED.
  4. For each value, it looks for any phrase of that value as whole words.
  5. The first value with a match wins. Step 2 then stops.
  6. A match replaces the model answer, whatever the model said.
  7. The list holds 35 phrases. Their confidence runs from 0.85 to 0.95.
  8. Every phrase confidence is above the review threshold of 0.7. A match alone never sends a call to review.

Step 3 — the language model

  1. The model receives the transcript with AGENT and USER labels, and the call summary.
  2. The platform uses the model answer only when no phrase matched.
  3. The platform discards an UNKNOWN answer. That answer never fills D2.
  4. An UNKNOWN answer for D2 can still send the call to review. See Outcome resolution.

What the record keeps

  1. The platform writes D2 into disposition_vector under the key d2Party.
  2. The entry holds the value, the confidence, the evidence and a source label.
  3. Step 1 writes the source retell_analysis and no evidence.
  4. Step 2 and step 3 both write the source llm.
  5. When a phrase replaces the model answer, the record keeps no copy of the model answer.
  6. To find a phrase match, compare the single evidence entry with the phrase list below.

The source label hides the phrase list. A count of D2 values by source puts every phrase match under llm. An accuracy figure for the model then includes the phrase list.

Workflow 2 — what each value does

A table of the D2 values in the order the phrase list checks them. Row 1, DECISION_MAKER, the person the call wants: 11 phrases at confidence 0.90, never set by step 1, and no headline, so the waterfall moves on. Row 2, GATEKEEPER, someone who screens the call: 9 phrases at 0.85, never set by step 1, no headline. Row 3, WRONG_PERSON, not the intended contact: 8 phrases at 0.95, the only value step 1 can set, headline wrong number. Row 4, REASSIGNED, the contact left this number: 7 phrases at 0.90, never set by step 1, headline wrong number. UNKNOWN comes only from the model and is never stored. D2 is step 4 of the waterfall, after D5, D6 and D1 and before D4 and D3. A transcript with a phrase from row 1 gets row 1 wherever that phrase sits.
Workflow 2 of 3 — the values. Four values in the order that step 2 checks them. Two reach the headline.
  1. D2 is step 4 of the headline waterfall. D5, D6 and D1 come first.
  2. WRONG_PERSON and REASSIGNED both give the headline wrong_number.
  3. DECISION_MAKER and GATEKEEPER give no headline. The waterfall moves on to D4, then D3.
  4. Steps 2 and 3 change the headline only when step 1 left it at needs_review.
  5. A later D2 value never replaces a headline that step 1 decided.
  6. Step 1 never sets D2 and D4 on the same call. D2 needs call_successful false, and D4 needs it true.
  7. A gauge counts D2 values over seven days for calls that reached a person.
  8. The gauge shows an empty D2 as UNRECORDED.
  9. The gauge also counts phrase matches from the agent’s own words.

Workflow 3 — the agent’s own words

One wrong-number call read by step 2. The agent says: Hi, this is Ava. Am I speaking with Sam? The caller says: No, you have got the wrong number. Step 2 joins both turns into one string and drops the speaker labels. It checks DECISION_MAKER first and finds the word speaking in the agent’s line, so it stops. GATEKEEPER, WRONG_PERSON and REASSIGNED are never checked, although wrong number would have matched WRONG_PERSON. The record stores DECISION_MAKER at confidence 0.9 with the source llm. The model answered WRONG_PERSON, and the record keeps no trace of that. The call gets no wrong number headline, and the waterfall moves on. With the agent’s turn removed, the same code returns WRONG_PERSON at 0.95. Measured on 10 September 2026 with the real extractor.
Workflow 3 of 3 — one wrong-number call. The agent’s question decides the value before the caller’s answer.
  1. The test ran the real extractor on the two turns in the diagram.
  2. A model stub answered WRONG_PERSON with confidence 0.9.
  3. The extractor returned DECISION_MAKER at 0.9, from the phrase speaking.
  4. The phrase came from the agent’s question, not from the caller.
  5. The same call without the agent’s turn returned WRONG_PERSON at 0.95.
  6. An August 2026 flow backup tells the agent to confirm that it is speaking with the merchant.
  7. An agent line with hold on sets GATEKEEPER in the same way.
  8. A real wrong-number call gets DECISION_MAKER this way, unless step 1 caught the wrong number.

A caller’s words out of context

  1. A second test used one caller line: I'm retired from the navy, I run the shop now. Tell me more.
  2. Step 2 matched retired and set REASSIGNED.
  3. The merged result had the headline wrong_number, confidence 0.9 and no review.
  4. The caller owned the business and asked to hear more.

The phrase list

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

OrderValueConfidencePhrases
1DECISION_MAKER0.90yes this is · speaking · that's me · you got the right person · i am the owner · i make the decisions · i handle that · i'm in charge · yes i am · this is he · this is she
2GATEKEEPER0.85let me transfer you · i'll get them · hold on · they're not available · can i take a message · i'm the assistant · i'm the secretary · who should i say is calling · i'll pass the message
3WRONG_PERSON0.95wrong number · wrong person · i don't know them · never heard of them · they don't live here · no one by that name · you've got the wrong · nobody here by that name
4REASSIGNED0.90no longer works here · left the company · moved on · doesn't work here anymore · retired · no longer with us · transferred to another

Known defects

  1. Step 2 reads the agent. The phrase list receives agent turns and caller turns as one string. Fix: pass only the caller’s turns.
  2. Value order beats word order. A transcript with yes this is and wrong number gets DECISION_MAKER. The position of each phrase does not matter.
  3. Short phrases match ordinary speech. speaking, hold on, moved on and retired occur in calls with the right person.
  4. A match erases the disagreement. The record keeps no copy of the model answer that the phrase replaced.
  5. One label for two sources. Step 2 and step 3 both write llm.
  6. The model call happens first. Every answered real call pays for a model request, and any phrase match then discards the answer.
  7. Names reach the model. Redaction matches fixed patterns only, and the call summary goes to the model unchanged.
  8. Step 1 needs an explicit field. An analysis without in_voicemail never gives WRONG_PERSON.

What rehearsal data can show

  1. On 10 September 2026, prod held 2,157,695 rehearsal attempts and 4 real attempts.
  2. None of the 4 real attempts holds a D2 value.
  3. A sample of about 1% of stage attempts held 240 D2 values. The phrase list set none of them.
  4. The impersonated model set 213 of them. Step 1 set the other 27.
  5. Rehearsal values show that the code path runs. They do not measure how well D2 reads a real caller.
  6. The corrections in the review queue are the only real measurement. See Outcome resolution.

D3 — intent — the same three sources, and five values.
D4 — qualification — the step 1 rule that no live agent satisfies.
Outcome resolution — how D2 fits among the seven dimensions.