> ## Documentation Index
> Fetch the complete documentation index at: https://80x.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Score leads into explainable bands

> A visible Formula combines fit, intent, and engagement into one lead score, writes it back to Attio, and routes each band to the right action.

A visible [Formula](/reference/calculations/formula) combines fit, intent, and engagement into one lead score, writes it back to Attio, and routes each band to the right action. Here is the flow in plain terms. A product or analytics event arrives on a [Webhook received](/reference/triggers/webhook-received) trigger. [Parse JSON](/reference/utilities/parse-json) unpacks the payload, the Formula reduces the three signals to a single number, and [Switch](/reference/conditions/switch) reads the band. [Update record](/reference/records/update-record) writes the result back to Attio, and the A band flows through [Round robin](/reference/workspace/round-robin) and [Enroll in sequence](/reference/sequences/enroll-in-sequence). The score is plain arithmetic, not an opaque model output. That means a sales development rep can open the run and read exactly why a lead is an A.

| | |
| - | - |
| **Difficulty** | Intermediate |
| **Time to build** | About 15 minutes |
| **Trigger** | [Webhook received](/reference/triggers/webhook-received), or [Attribute value changed](/reference/triggers/attribute-value-changed) when the signals already live in Attio |
| **Blocks used** | [Parse JSON](/reference/utilities/parse-json), [Formula](/reference/calculations/formula), [Switch](/reference/conditions/switch), [Update record](/reference/records/update-record), [Round robin](/reference/workspace/round-robin), [Enroll in sequence](/reference/sequences/enroll-in-sequence) |
| **Credit cost per run** | 1 credit on the C band, up to 3 credits on the A band |
| **Plan required** | Plus or higher |

## Prerequisites

* Attio **Plus** plan or higher, which runs the new Workflows engine.
* A `Person` object (or `Company`) with three attributes already created. You need `lead_score` as a number, and `lead_band` as a single-select with options `A`, `B`, and `C`. You also need a matching key such as `email` or `domain`.
* A source for the event payload. The webhook path needs a product-analytics or reverse-ETL tool that can POST JSON to Attio's generated webhook URL. Reverse-ETL means extract, transform, load. The attribute path needs the signal fields to already be writing to the record.
* An active sequence to enroll A-band leads into, and a sales team configured for round-robin assignment.
* Workspace member access with permission to publish workflows. The webhook source is the only external credential you need. No API keys are required inside Attio.

## How it works

The trigger fires on one event. Parse JSON turns the raw payload into typed variables. That means each value carries its proper type, such as a number rather than text. The Formula then reduces fit, intent, and engagement to one number on a 0 to 100 scale. Switch reads the resulting band and sends the run down exactly one branch.

Every branch writes the score and band back to the record, so the value is durable and filterable. The A band also picks an owner and starts outreach. The C band stops after the write. Each step passes its output to the next through the engine's variables, which the [Variables & data passing](/explanation/variables) concept explains in full.

1. [Webhook received](/reference/triggers/webhook-received) fires on the inbound event, or [Attribute value changed](/reference/triggers/attribute-value-changed) fires on a signal field.
2. [Parse JSON](/reference/utilities/parse-json) extracts `fit`, `intent`, and `engagement`.
3. [Formula](/reference/calculations/formula) computes the combined `lead_score`.
4. [Switch](/reference/conditions/switch) branches on the score band.
5. [Update record](/reference/records/update-record) writes `lead_score` and `lead_band` on every branch.
6. On the A band only, [Round robin](/reference/workspace/round-robin) assigns an owner and [Enroll in sequence](/reference/sequences/enroll-in-sequence) starts outreach.

## Build steps

<Steps>
  <Step title="Add the trigger">
    Create a new workflow and add the **Webhook received** trigger. Copy the generated webhook URL into your analytics tool so it can POST events to the workflow. When your signals already live in Attio, swap in the **Attribute value changed** trigger instead. Scope it to the `Person` object and the signal attribute.

    | Field | Value |
    | - | - |
    | Trigger | Webhook received |
    | Object context | Person |
    | Sample payload key `email` | `jane@acme.com` |
    | Sample payload key `company_domain` | `acme.com` |
  </Step>

  <Step title="Parse the payload">
    Add the **Parse JSON** block. Point its **Source** field at the trigger body, then define a schema with the three signal inputs and the matching key. Mark anything your source may omit as optional, so a missing field does not fail the run.

    | Field | Value |
    | - | - |
    | Source | `{{ trigger.body }}` |
    | Schema field | `email` (string, required) |
    | Schema field | `fit` (number, 0–40) |
    | Schema field | `intent` (number, 0–40) |
    | Schema field | `engagement` (number, 0–20) |
  </Step>

  <Step title="Compute the score with a visible Formula">
    Add the **Formula** block. Keep the expression readable so reps trust the result. Transparency is the entire point of this recipe, unlike a black-box machine-learning score where the math is hidden. Each component is capped, so the total lands on a clean 0 to 100 scale.

    | Field | Value |
    | - | - |
    | Output name | `lead_score` |
    | Expression | `min(fit, 40) + min(intent, 40) + min(engagement, 20)` |
    | Output type | Number |
    | Example result | `82` (fit 38, intent 32, engagement 12) |
  </Step>

  <Step title="Branch on the band with Switch">
    Add the **Switch** block and create one branch per band, reading the score from the Formula step. Order matters here. Switch takes the first matching branch, so put the highest threshold first.

    | Field | Value |
    | - | - |
    | Input | `{{ formula.lead_score }}` |
    | Branch A | `lead_score >= 75` |
    | Branch B | `lead_score >= 50` |
    | Branch C (default) | Everything else |
  </Step>

  <Step title="Write the score and band on every branch">
    On each Switch branch, add an **Update record** block targeting the matched `Person`. Write both the numeric score and the band, so the value is filterable and visible in the record. The band string differs per branch.

    | Field | Value |
    | - | - |
    | Record | `{{ trigger.record }}`, matched by `email` |
    | `lead_score` | `{{ formula.lead_score }}` |
    | `lead_band` | `A` on Branch A, `B` on Branch B, `C` on Branch C |
  </Step>

  <Step title="Route the A band">
    On Branch A only, after Update record, add a **Round robin** block to assign an owner from your sales pool. Then add an **Enroll in sequence** block to start outreach. Branch B adds the lead to a nurture sequence or list. Branch C ends after the score write.

    | Field | Value |
    | - | - |
    | Round robin pool | Account executives, EMEA |
    | Assignment target | `Owner` on the matched record |
    | Sequence | Proposal outbound, A leads |
    | Enrolled record | `{{ trigger.record }}` |
  </Step>
</Steps>

## Variable mapping

| Variable | Source block | Field | Example value |
| - | - | - | - |
| `email` | Parse JSON | `email` | `jane@acme.com` |
| `fit` | Parse JSON | `fit` | `38` |
| `intent` | Parse JSON | `intent` | `32` |
| `engagement` | Parse JSON | `engagement` | `12` |
| `lead_score` | Formula | `lead_score` | `82` |
| `lead_band` | Switch | Matched branch | `A` |
| `assigned_owner` | Round robin | `Owner` | Priya Sharma |

## Credit cost worked example

Parse JSON, Formula, and Switch are free logic blocks, so they cost nothing no matter how often they run. Only the data writes and the sequence enrollment consume credits.

| Branch | Billable blocks | Credits per run |
| - | - | - |
| A | Update record + Round robin¹ + Enroll in sequence | 3 |
| B | Update record + Enroll in nurture sequence | 2 |
| C | Update record | 1 |

¹ Round robin is a free workspace block. The 3 credits on the A branch come from the Update record and Enroll in sequence writes.

Say you process 2,000 events a month with a typical split of 10 percent A, 30 percent B, and 60 percent C. The monthly cost is `(200 × 3) + (600 × 2) + (1,200 × 1) = 3,000` credits. Compare that against your plan's monthly credit allowance on [Credits & pricing explained](/reference/credits-and-pricing). Adjust the band thresholds if the A branch enrolls more leads than your team can work. None of this involves an AI block.

If you later add a [Classify record](/reference/ai/classify-record) step behind a [Filter](/reference/conditions/filter), sample about ten records and read the per-run cost in the run viewer. AI cost is variable and token-based, so it is not a fixed number you can read off a table.

## Test plan

1. Open the trigger in the workflow editor and click the **Run test** button. For the webhook path, paste a sample payload with `email`, `fit`, `intent`, and `engagement`. For the attribute path, edit the signal field on a test `Person`.
2. Confirm **Parse JSON** outputs typed values, such as `fit: 38`, `intent: 32`, and `engagement: 12`, and not raw strings.
3. Confirm the **Formula** output `lead_score` reads `82` for those inputs.
4. Confirm **Switch** routes the run down Branch A, because `82 >= 75`.
5. Open the matched record and confirm **Update record** wrote `lead_score = 82` and `lead_band = A`.
6. Confirm **Round robin** assigned an owner and **Enroll in sequence** shows the lead enrolled.
7. Open the run in run history and read each block's input and output to confirm the score is explainable end to end. The [Runs, debugging & publishing](/explanation/runs-and-debugging) concept shows where each value appears.
8. Fire one deliberately failing input. Send a payload where `intent` is the string `"high"` instead of a number. Parse JSON fails the schema and stops the run before any write. Confirm in run history that no credits were spent and no record was updated.

## Failure modes

| Symptom | Cause | Fix |
| - | - | - |
| The run stops at Parse JSON | A signal field arrived as a string or was missing | Mark optional fields optional in the schema and coerce numeric fields at the source. |
| Every lead lands in C | Formula inputs resolve to `0` because the variable names do not match the Parse JSON output | Align the Formula variable names with the exact Parse JSON output keys. |
| The score looks wrong but reps cannot tell why | The component caps are missing, so one signal dominates the total | Keep the `min(...)` caps visible in the expression and document the scale on `lead_band`. |
| A-band leads are enrolled but not assigned | The Round robin pool is empty or its members are inactive | Confirm the pool has active members, and run Round robin before Enroll in sequence on the branch. |
| Duplicate enrollments appear | The trigger re-fires for the same event | Add a free [Filter](/reference/conditions/filter) that exits when `lead_band` is already `A`. |

## Variations

### Separate fit and intent scores

Output two Formula values instead of one combined number. Name them `fit_score` and `intent_score`, and write both to the record. Switch on a two-by-two matrix, where high fit and high intent together earn an A. That way, reps can see whether a lead is a good fit, actively buying, or both. Everything else stays the same.

### Recompute on a schedule

Swap the trigger for [Recurring schedule](/reference/triggers/recurring-schedule). Add a [Find records](/reference/records/find-records) block to pull the leads to rescore. Then wrap the Formula, Switch, and Update record steps in a [Loop](/reference/utilities/loop). The loop multiplies cost by the number of records. Gate it with a [Filter](/reference/conditions/filter) that rescores only leads whose signals changed since the last run.

## See also

* [Webhook lead intake with safe upsert](/guides/capture-and-qualify/webhook-lead-intake)
* [Variables & data passing](/explanation/variables)
* [Runs, debugging & publishing](/explanation/runs-and-debugging)
* [Switch](/reference/conditions/switch)
* [Formula](/reference/calculations/formula)

*Source: [Attio Help Center, Workflows](https://attio.com/help/workflows). Last reviewed against the Attio Help Center, June 2026.*


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.