Use cases / Customer analytics

Which deals are most likely to close

Which deals close this quarter, and what could stop them.

Unit
One opportunity, all its calls this quarter
Ask
Next step? Objection? Stage? Then: likely to close by the date?
Get
Ranked deals with p(close) and the top risk

Fireflies · Attio · 2 reducers · 5 questions · updated Sep 21, 2026

The flow

0 calls source snapshot v8
0 Call signals reducer requests, 3 questions each
0 Group by opportunity group no model call
0 Attach the record join ARR, close date, stage
0 Close likelihood reducer requests, 2 questions each
0 Outputs output ranked by p(close)

Score every call once. Then score every deal with its calls and its record. 137 requests for 96 calls and 41 deals. Replay last quarter for the calibration table.

Follow one record

What comes out

Pipeline reviewrun 91c4e0aa, 41 rows, sorted by p(close)
Opportunityp(close)Latest stageNext stepTop riskClose dateARR
Gable Manufacturing0.91Verbal commitmentyes 0.94none2026-09-2636,000
Halyard Freight0.79Negotiationyes 0.88pricing2026-09-3048,000
Brightwater Labs0.64Validationyes 0.71security review2026-10-1422,000
Corvid Systems0.41Evaluationno 0.22timing2026-10-3160,000
Tidewater Legal0.22Discoveryno 0.09no decision owner2026-11-1518,000
Fennel & Co0.12Evaluationno 0.15champion left2026-09-309,000

Synthetic rows in the app's Outputs layout. ARR and dates come from the joined Attio record, not from the model.

Details, if you want them

whyWhy this shape

Why this shape

  • Per call first. Next step, objection, stage. One call is the natural unit.
  • Per deal second. All its calls plus the record. A true dependency, so a second stage.
  • Between them, no model calls. Group, then join.

Make the probability earn its threshold

  1. Run the same definition on last quarter’s snapshots.
  2. Join closed-won and closed-lost on opportunity_id.
  3. Bucket by p(close). Count won and lost per bucket.
  4. Read the table. That is what 0.8 means for your team.
p(close)dealswonshare
0.90 and upyour numberyour numberyour number
0.70 to 0.89your numberyour numberyour number
0.50 to 0.69your numberyour numberyour number
below 0.50your numberyour numberyour number
questionsThe 5 questions, verbatim
  • Call signals next_stepbinary, noul

    Did the prospect in `transcript` commit to a specific next step with a named owner or a date?

  • Call signals objectioncategorical, choice

    Which objection, if any, dominates `transcript`?

    pricingsecurity and complianceintegration efforttimingnone raised
  • Call signals stageordinal, score

    How far along is the buying process as evidenced in `transcript`?

    DiscoveryEvaluationValidationNegotiationVerbal commitment
  • Close likelihood closebinary, noul

    Given the call signals in `calls` and the record in `opportunity`, is this opportunity likely to close by `opportunity.close_date`?

  • Close likelihood riskcategorical, choice

    What is the single biggest risk to closing, based on `calls` and `opportunity`?

    pricingsecurity reviewchampion leftno decision ownertimingnone
buildHow each stage is built, as JSON
1 reducer Call signals
One request per transcript. Three independent questions share the state. stage is an ordinal over your own sales stages, so the expected value is a position on that scale, and the distribution shows when a call sits between two stages.

stage definition

{
  "id": "call_signals",
  "input": "source:calls",
  "kind": "evaluate",
  "model": "jev",
  "name": "Call signals",
  "questions": {
    "next_step": {
      "instructions": "Did the prospect in `transcript` commit to a specific next step with a named owner or a date?",
      "type": "noul"
    },
    "objection": {
      "criteria": {
        "integration_effort": "Effort to integrate or migrate",
        "none_raised": "No objection was raised",
        "pricing": "Price, discount or budget",
        "security_compliance": "Security review, compliance or data handling",
        "timing": "Not now, later quarter, other priorities"
      },
      "instructions": "Which objection, if any, dominates `transcript`?",
      "type": "choice"
    },
    "stage": {
      "criteria": [
        "Discovery",
        "Evaluation",
        "Validation",
        "Negotiation",
        "Verbal commitment"
      ],
      "instructions": "How far along is the buying process as evidenced in `transcript`?",
      "type": "score"
    }
  },
  "state_path": []
}
2 group Group by opportunity
Deterministic. Every call now carries its signals; grouping on opportunity_id collects them into one unit per deal. The members are kept, so the trace shows which call said what.

stage definition

{
  "id": "by_opportunity",
  "input": "call_signals",
  "key": [
    "opportunity_id"
  ],
  "kind": "group",
  "name": "Group by opportunity"
}
3 join Attach the record
A join against the Attio snapshot brings the fields the model should weigh: ARR, the stated close date, the CRM stage, the owner. Nothing is fetched live. The snapshot is the one the run pinned.

stage definition

{
  "id": "with_record",
  "input": "by_opportunity",
  "kind": "join",
  "left_key": [
    "input",
    "key"
  ],
  "name": "Attach the record",
  "right": "source:opportunities",
  "right_key": [
    "id"
  ]
}
4 reducer Close likelihood
The second reducer has a true dependency: it needs the first stage’s answers across several calls and the joined record. One request per opportunity. close is a probability conditioned on exactly this evidence; risk names the thing most likely to stop it.

stage definition

{
  "id": "close",
  "input": "with_record",
  "kind": "evaluate",
  "model": "jev",
  "name": "Close likelihood",
  "questions": {
    "close": {
      "instructions": "Given the call signals in `calls` and the record in `opportunity`, is this opportunity likely to close by `opportunity.close_date`?",
      "type": "noul"
    },
    "risk": {
      "criteria": {
        "champion_left": "The internal champion is gone or disengaged",
        "no_decision_owner": "Nobody with authority has committed",
        "none": "No material risk visible",
        "pricing": "Price or budget",
        "security_review": "An open security or compliance review",
        "timing": "The buyer's timeline slipped"
      },
      "instructions": "What is the single biggest risk to closing, based on `calls` and `opportunity`?",
      "type": "choice"
    }
  },
  "state_path": []
}
5 definition The whole workflow, as saved

Saving publishes an immutable version. A run freezes this definition, the snapshot IDs and each reducer’s model profile, so the trace can always show exactly what ran. Edits create a new version; old runs keep theirs.

jevreduce.local.v1

{
  "name": "Which deals are most likely to close",
  "schema_version": "jevreduce.local.v1",
  "sources": {
    "calls": "snap_fireflies_calls_v8",
    "opportunities": "snap_attio_opps_v5"
  },
  "stages": [
    {
      "id": "call_signals",
      "input": "source:calls",
      "kind": "evaluate",
      "model": "jev",
      "name": "Call signals",
      "questions": {
        "next_step": {
          "instructions": "Did the prospect in `transcript` commit to a specific next step with a named owner or a date?",
          "type": "noul"
        },
        "objection": {
          "criteria": {
            "integration_effort": "Effort to integrate or migrate",
            "none_raised": "No objection was raised",
            "pricing": "Price, discount or budget",
            "security_compliance": "Security review, compliance or data handling",
            "timing": "Not now, later quarter, other priorities"
          },
          "instructions": "Which objection, if any, dominates `transcript`?",
          "type": "choice"
        },
        "stage": {
          "criteria": [
            "Discovery",
            "Evaluation",
            "Validation",
            "Negotiation",
            "Verbal commitment"
          ],
          "instructions": "How far along is the buying process as evidenced in `transcript`?",
          "type": "score"
        }
      },
      "state_path": []
    },
    {
      "id": "by_opportunity",
      "input": "call_signals",
      "key": [
        "opportunity_id"
      ],
      "kind": "group",
      "name": "Group by opportunity"
    },
    {
      "id": "with_record",
      "input": "by_opportunity",
      "kind": "join",
      "left_key": [
        "input",
        "key"
      ],
      "name": "Attach the record",
      "right": "source:opportunities",
      "right_key": [
        "id"
      ]
    },
    {
      "id": "close",
      "input": "with_record",
      "kind": "evaluate",
      "model": "jev",
      "name": "Close likelihood",
      "questions": {
        "close": {
          "instructions": "Given the call signals in `calls` and the record in `opportunity`, is this opportunity likely to close by `opportunity.close_date`?",
          "type": "noul"
        },
        "risk": {
          "criteria": {
            "champion_left": "The internal champion is gone or disengaged",
            "no_decision_owner": "Nobody with authority has committed",
            "none": "No material risk visible",
            "pricing": "Price or budget",
            "security_review": "An open security or compliance review",
            "timing": "The buyer's timeline slipped"
          },
          "instructions": "What is the single biggest risk to closing, based on `calls` and `opportunity`?",
          "type": "choice"
        }
      },
      "state_path": []
    }
  ]
}
limitsWhat it will not do
  • p(close) is conditioned on these calls and this record. Not a pipeline forecast.
  • Calibrate on last quarter before you threshold.
  • Transcription errors propagate.
  • No live CRM reads, no write-back. Your code does that.
  • No follow-up emails. Jev does not write prose.
relatedRelated pages

Try it on your own records.