Skip to main content
This tutorial takes one lead through the Decision API with the model that reads journeys. Every response on this page is the real answer of the served model to the request above it. The model is Lev 2, the second generation of Lev. It is deployed under the name fin, and the registry and some other pages call it Fin 2: it is the same model. Like any decision model it scores, it does not write. Three things set it apart from a plain classifier, and each has a step below:
  1. It reads the lead’s history, cut at the moment of the decision, beside the call.
  2. Every answer comes with the events it came from, and that is tested by changing one event and checking that the answer changes.
  3. It answers what would happen under each action, doing nothing included, and the decision step records the probability of the action it took.
Why this is not a plain classifier explains the difference for an engineer.
The responses on this page were recorded on fin2-v5. The release served since 6 October 2026, fin2-v7, answers the same requests with other numbers, and it has seven questions of its own about a journey (the customer’s interest now, an open objection, how engaged they have been, what the latest sales note says, our last step, the offers we sent, how much we contacted them). It also reads our own earlier decisions, so write them the way its training journeys do: the decision’s line with what a rule shut, then a row for what was done (Writing it back). GET $API/health lists the fields and moves the served release knows, and the Decide playground checks every request it sends against that release’s schema.

Before you start

The version at the end of the URL goes up with every deploy, so read it from levers9 decide status, do not pin it. The routes used here: The dashboard’s POST /api/rl/decide, /api/rl/decide/batch and /api/rl/recommend take the same state, questions, decision and explain, and hold the token for you.

The lead

One synthetic lead, written for this page: no client data. Five days of history, then a call. The history starts at the lead’s first call: before a first call’s transcript the system knows nothing about a customer. Lev is our own model, which labelled that call. The call that has just ended, on 27 August at 18:15:

1. One call

The simplest use: a call and a question. Send the call as a list of turns, never one joined string. call.json:
  • choice is the answer, probabilities has one number per option of the question and they add up to one.
  • answer_confidence is the model’s own probability of the chosen option before the “none of these” slot is taken out. When “none of these” wins, the answer carries "low_confidence": true: none of your options fits.
  • latency_ms is the model’s time on the card. The round trip adds network time.
  • Ask as many questions as you like in one request: all of them are answered in the same pass.
POST $API/v1/systemone with "model": "lev-latest" returns the same answers in the System One shape.

2. The call with the lead’s journey

To give the model the history, send the state as an object with three parts: An event: journey.json (three of the nine questions shown; every option’s description is a short sentence, as in step 1):
source is free text: write who produced each event. Here Lev is our own model, which labelled the first call. Each answer now carries evidence: the three events it read most, with their share of its attention.
Asked nine questions about this lead in one request (44 ms of model time), it answered: Without the call (segments left out), interest_now comes back warm (0.99), the last reading in the history: the “hot” above is read from what the customer said in the call. So the same request serves a trigger with no new call, such as “no reply in 48 hours”: send the journey and the decision time alone.

3. Check that it reads causes

An answer that names an event is a claim, and it can be tested: take one event away and ask again. This is a twin: the same lead in every respect but one. Twin A: the reviewer’s correction never happened. Send the same request without ev-4 (“Reviewer changed objection from price to none”). Two answers change, and only those two: The other seven answers are the same as before. Without the correction, the model’s label of a price objection stands, and an offer is no longer likely to work. Twin B: the sales note never happened. Send the request without ev-7. One answer changes: This is the test to run on your own data before you trust the model on it: for a few leads you know well, remove or change the event you believe decides an answer, and check that the answer follows. An answer that does not move was not read from that event. On our test set of synthetic journeys, 96% of the answers that should change do, and 99% of the others stay; through this endpoint, on 60 pairs, 96% and 100%. The Decide page’s causality builder does the same by hand.

4. Ask what it read

Add "explain": true and the response carries, beside the answers, what the model actually read. Here one more event is added to the journey, dated after the decision:
  • read is every line in the order the model read it. Each event became one line with its order, its day before the decision, its source and type, its text and its provenance; the call’s turns follow.
  • dropped says what was not read and why. ev-9 is after the decision time, so it was cut before the model saw anything: the model cannot be told the future by accident. A journey longer than 64 lines keeps its newest events and folds the older ones into one summary line; those are listed here too.
  • evidence_all gives every event’s share for each answer, and attention_on_history how much of the answer’s attention was on the journey and how much on the call.
Attention is a clue to what was read, not proof of cause. The proof is the twin of step 3.

5. Ask for a decision

Add a decision block: the actions you could take, what a success through each is worth, what each costs, your rules and your objective. The response then carries a recommendation. The block is described in full on Recommendations; here it is on our lead.
Read it from the top:
  • p_success is the chance that this lead reserves if that action is taken now. The request gave none, so the model answered one question per candidate (chance_source: "model").
  • uplift is what the action adds over doing nothing. This lead is very likely to reserve whatever happens (0.91 if left alone), so no action adds anything, and with the objective uplift the step recommends doing nothing. A model that only predicted “will this lead reserve” would rank this lead first and spend an offer on it. That is the point of asking for the baseline.
  • why is written from the numbers, never invented.
  • ledger_event is the event to append to the lead’s journey, so the next request sees what was done. Its text is one line; with "ledger": "complete" in the decision block it says the whole decision (what was read, every option’s chance, what the rules shut, how the pick was made), and its record holds the same as data either way. See Writing it back.
Exploration. With "exploration": 0.1 the same request came back with "action": "no_action", "explored": false and "propensity": 0.925. One time in ten the step draws an allowed action at random, so with four allowed the best one is taken with probability 0.925 and each of the others with 0.025; when another one is taken the response says "explored": true and carries its 0.025. Keep that propensity with the outcome. It is what lets you measure later, from your own logs, what each action really changes. Your own chances. Pass chances and the model is not asked for them:
The model’s own chances per action are right on average and nearly the same for every kind of lead: on simulated leads it gives about the same lift to a lead an offer would tip as to one it would push away. Use them to see the step work. For decisions that matter, pass chances from your own estimate until the outcome table is served (see Limits).

Three ways to use it

After every call. Step 1, with your tenant’s own questions. Use /decide/batch to score every prefix of a call, or a day of calls, in one pass. At a trigger with no call. “No reply in 48 hours”, “viewing attended yesterday”: send the journey and the decision time with no segments, and ask where the lead stands (next_stage, interest_now, sales_cue, worth_calling_again). The four came back for our lead as contacted, warm, ready and yes. A review queue. Send to a person the answers with low_confidence, the recommendations with "review": true, and any answer whose evidence points at an event a person has since corrected. The reviewer sees the events behind the answer, not only the answer.

Why this is not a plain classifier

A discriminative model learns to map an input to a label. That is enough to say what a call was about. It is not enough to decide what to do with a lead, for three reasons, and Lev 2 is built around them. In one sentence: a classifier answers “what is true of this lead”, and a decision needs “what changes if we act”. The first is a reading task. The second is about a future that is only seen for the action that was taken, so it needs the cut in time, the test by twins, a do-nothing baseline and the recorded probability of each action. Those four are what “causal” means on this page.

Questions it answers best

The model reads any question from its text, but it answers best the ones it was taught, asked with the same id and the same option keys: For a tenant’s own call questions (its outcome and sentiment), use the option keys its schema lists.
A question it was not taught gets an answer that looks just as confident. Asked of this lead “Did the customer ask about parking?”, it said yes with 0.9955. The customer asked for the brochure and the payment plan. Measure any new question on labelled examples before you use it.

Limits

  • Every journey it has been measured on is synthetic. Nothing has been tested on a tenant’s real journeys yet. Run the twin test of step 3 on your own leads first.
  • Its chances per action are right on average, not per lead. The plan is an outcome table built from your logged decisions on the model’s answers; on simulated leads it orders leads by the effect of an action far better than the model’s own chances (0.84 against 0.48 on our measure, where 1 is a perfect order). It is not a part of the Decision API yet. Until then, pass your own chances.
  • Two rare objections, location and financing, are not told apart. It knows an objection that is not the price is open, and not which one.
  • A label it has never seen needs examples. Rewording a taught option works; a new option does not.
  • Events need ids and times. Without an id there is no evidence to point at; without a time an event cannot be cut and is always read.
  • A wrong field is answered with a sentence. A rule that names an unknown action, an objective that needs a baseline when none is given, a chance outside 0 to 1: 422 with the field named, for example objective uplift needs a baseline: name the do-nothing candidate in decision.baseline, or include no_action.