Quote debugging

See exactly how every price was built

Alongside the result, the Ptally engine returns a debug block: every input it read, every lookup, every formula with the values it used, every rule it checked and how long each step took. Like the models, it is plain JSON that an AI agent can trace and a person can read. This page walks through a real one.

Readable by people and AI

AI friendly. Human readable.

People and AI agents read the same debug block. When a price looks wrong, both see the same steps, values and warnings.

For AI
Easy for agents to trace
Each step uses the same id as in the model, and each formula lists the values it used. An agent can follow the trace to the first wrong value and name the step and table row to fix.
For people
Reads like a worksheet
Rules keep their plain-English names and formulas show their numbers. An underwriter can recalculate a premium by hand or see why a quote was referred.
For both
Easy to check
Everything is in the response, with no separate logs. You see what the agent saw, so you can check its diagnosis before a fix goes into a draft.
The example

A French Bulldog on £7,000 cover

Priced against the pet insurance model. It comes back valid at £621.47 but referred, because £7,000 cover on a high-risk breed needs an underwriter. The debug block shows why.

Quote input
{
  "species": "dog",
  "breed": "french_bulldog",
  "pet_age": 2,
  "region": "south_east",
  "vet_fee_limit": 7000,
  "excess": 100,
  "neutered": true
}
Response output, debug collapsed
{
  "risk_premium": 554.88,
  "ipt": 66.59,
  "monthly_premium": 51.79,
  "base_premium": 298,
  "refer": true,
  "result": 621.47,
  "valid": true,
  "debug": {
    "meta": "{ … }",
    "timer": "[ 14 entries ]",
    "errors": "[]",
    "exclusions": "[]",
    "refers": "[ 1 entry ]",
    "excesses": "[]",
    "endorsements": "[]",
    "indicators": "{}",
    "steps": "{ 15 steps }"
  }
}
Show the full response (5.1 KB)
{
  "risk_premium": 554.88,
  "ipt": 66.59,
  "monthly_premium": 51.79,
  "base_premium": 298,
  "refer": true,
  "result": 621.47,
  "valid": true,
  "debug": {
    "meta": {
      "name": "UK Pet Insurance: Cats and Dogs",
      "description": "Lifetime vet-fee cover for a single cat or dog, rated by species, breed, age, region, vet-fee limit and excess.",
      "summary": "Rates annual lifetime vet-fee cover for one cat or dog in the UK. Base rates by species and annual vet-fee limit are adjusted for age band, breed, region, voluntary excess and neutering, then 12% Insurance Premium Tax is added. Older pets and breed/species mismatches are declined; high-limit cover on high-risk breeds is referred."
    },
    "timer": [
      {
        "time": 0,
        "elapsed": 0.88,
        "key": "input"
      },
      {
        "time": 0.698,
        "elapsed": 0.698,
        "key": "base_rates"
      },
      {
        "time": 0.864,
        "elapsed": 0.166,
        "key": "age_bands"
      },
      {
        "time": 0.986,
        "elapsed": 0.122,
        "key": "breed_factors"
      },
      {
        "time": 1.019,
        "elapsed": 0.033,
        "key": "region_factors"
      },
      {
        "time": 1.247,
        "elapsed": 0.228,
        "key": "excess_factors"
      },
      {
        "time": 1.285,
        "elapsed": 0.038,
        "key": "neuter_factors"
      },
      {
        "time": 2.672,
        "elapsed": 1.387,
        "key": "risk_premium"
      },
      {
        "time": 3.06,
        "elapsed": 0.388,
        "key": "ipt"
      },
      {
        "time": 3.248,
        "elapsed": 0.187,
        "key": "total_premium"
      },
      {
        "time": 3.372,
        "elapsed": 0.125,
        "key": "monthly_premium"
      },
      {
        "time": 3.549,
        "elapsed": 0.177,
        "key": "pet_exclusions"
      },
      {
        "time": 3.62,
        "elapsed": 0.071,
        "key": "pet_refers"
      },
      {
        "time": 3.927,
        "elapsed": 0.307,
        "key": "output"
      }
    ],
    "errors": [],
    "exclusions": [],
    "refers": [
      {
        "step": "pet_refers",
        "name": "£7,000 limit on a high-risk breed",
        "vet_fee_limit": 7000,
        "breed": "french_bulldog",
        "exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
        "result": true
      }
    ],
    "excesses": [],
    "endorsements": [],
    "indicators": {},
    "steps": {
      "statics": {},
      "input": {
        "errors": [],
        "items": [
          {
            "step": "input",
            "name": "Species",
            "species": "dog",
            "exp": "{{species}}",
            "result": "dog"
          },
          {
            "step": "input",
            "name": "Breed",
            "breed": "french_bulldog",
            "exp": "{{breed}}",
            "result": "french_bulldog"
          },
          {
            "step": "input",
            "name": "Pet age (years)",
            "pet_age": 2,
            "exp": "{{pet_age}}",
            "result": 2
          },
          {
            "step": "input",
            "name": "Region",
            "region": "south_east",
            "exp": "{{region}}",
            "result": "south_east"
          },
          {
            "step": "input",
            "name": "Annual vet-fee limit (£)",
            "vet_fee_limit": 7000,
            "exp": "{{vet_fee_limit}}",
            "result": 7000
          },
          {
            "step": "input",
            "name": "Excess (£)",
            "excess": 100,
            "exp": "{{excess}}",
            "result": 100
          },
          {
            "step": "input",
            "name": "Neutered",
            "neutered": true,
            "exp": "{{neutered}}",
            "result": true
          }
        ],
        "output": {
          "species": "dog",
          "breed": "french_bulldog",
          "pet_age": 2,
          "region": "south_east",
          "vet_fee_limit": 7000,
          "excess": 100,
          "neutered": true
        }
      },
      "base_rates": {
        "errors": [],
        "items": [],
        "output": {
          "base_premium": 298
        }
      },
      "age_bands": {
        "errors": [],
        "items": [],
        "output": {
          "pet_age_factor": 1
        }
      },
      "breed_factors": {
        "errors": [],
        "items": [],
        "output": {
          "breed_factor": 1.75,
          "breed_matches": 1
        }
      },
      "region_factors": {
        "errors": [],
        "items": [],
        "output": {
          "region_factor": 1.12
        }
      },
      "excess_factors": {
        "errors": [],
        "items": [],
        "output": {
          "excess_factor": 1
        }
      },
      "neuter_factors": {
        "errors": [],
        "items": [],
        "output": {
          "neuter_factor": 0.95
        }
      },
      "risk_premium": {
        "errors": [],
        "items": [
          {
            "formula": "round({{base_premium}} * {{pet_age_factor}} * {{breed_factor}} * {{region_factor}} * {{excess_factor}} * {{neuter_factor}}, 2)",
            "result": 554.88,
            "base_premium": 298,
            "pet_age_factor": 1,
            "breed_factor": 1.75,
            "region_factor": 1.12,
            "excess_factor": 1,
            "neuter_factor": 0.95
          }
        ],
        "output": {
          "risk_premium": 554.88
        }
      },
      "ipt": {
        "errors": [],
        "items": [
          {
            "formula": "round({{risk_premium}} * 0.12, 2)",
            "result": 66.59,
            "risk_premium": 554.88
          }
        ],
        "output": {
          "ipt": 66.59
        }
      },
      "total_premium": {
        "errors": [],
        "items": [
          {
            "formula": "round({{risk_premium}} + {{ipt}}, 2)",
            "result": 621.47,
            "risk_premium": 554.88,
            "ipt": 66.59
          }
        ],
        "output": {
          "total_premium": 621.47
        }
      },
      "monthly_premium": {
        "errors": [],
        "items": [
          {
            "formula": "round({{total_premium}} / 12, 2)",
            "result": 51.79,
            "total_premium": 621.47
          }
        ],
        "output": {
          "monthly_premium": 51.79
        }
      },
      "pet_exclusions": {
        "errors": [],
        "items": [
          {
            "step": "pet_exclusions",
            "name": "Pet older than 14 at inception",
            "pet_age": 2,
            "exp": "{{pet_age}} > 14",
            "result": false
          },
          {
            "step": "pet_exclusions",
            "name": "Breed does not match species",
            "breed_matches": 1,
            "exp": "{{breed_matches}} == 0",
            "result": false
          }
        ],
        "output": {
          "pet_exclusions": false
        }
      },
      "pet_refers": {
        "errors": [],
        "items": [
          {
            "step": "pet_refers",
            "name": "£7,000 limit on a high-risk breed",
            "vet_fee_limit": 7000,
            "breed": "french_bulldog",
            "exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
            "result": true
          }
        ],
        "output": {
          "pet_refers": true
        }
      },
      "output": {
        "errors": [],
        "items": [
          {
            "step": "output",
            "name": "Risk premium",
            "risk_premium": 554.88,
            "exp": "{{risk_premium}}",
            "result": 554.88
          },
          {
            "step": "output",
            "name": "IPT",
            "ipt": 66.59,
            "exp": "{{ipt}}",
            "result": 66.59
          },
          {
            "step": "output",
            "name": "Monthly premium",
            "monthly_premium": 51.79,
            "exp": "{{monthly_premium}}",
            "result": 51.79
          },
          {
            "step": "output",
            "name": "Base premium",
            "base_premium": 298,
            "exp": "{{base_premium}}",
            "result": 298
          },
          {
            "step": "output",
            "name": "Refer to underwriter",
            "pet_refers": true,
            "exp": "{{pet_refers}}",
            "result": true
          },
          {
            "formula": "round({{total_premium}}, 2)",
            "result": 621.47,
            "total_premium": 621.47
          },
          {
            "step": "result",
            "pet_exclusions": false,
            "exp": "{{pet_exclusions}} == false",
            "result": true
          }
        ],
        "output": {
          "risk_premium": 554.88,
          "ipt": 66.59,
          "monthly_premium": 51.79,
          "base_premium": 298,
          "refer": true,
          "result": 621.47,
          "valid": true
        }
      }
    }
  }
}
01result · valid
The result

result is the price from the model's result formula. Here that is the annual premium including IPT. valid is false if any exclusion fired; a declined quote still carries a price, but it must not be bound.

Everything else is the model's own output fields: the premium split, the monthly figure and refer, which is true here. Those are the numbers to show a customer or put on a schedule.

{
  "risk_premium": 554.88,
  "ipt": 66.59,
  "monthly_premium": 51.79,
  "base_premium": 298,
  "refer": true,
  "result": 621.47,
  "valid": true
}
02debug.meta
Which model priced it

The model's name, description and summary at the moment it priced the quote. Combined with the version on the quote response, it ties every price to an exact model, so you can always answer “what was live when this was quoted?”

{
  "name": "UK Pet Insurance: Cats and Dogs",
  "description": "Lifetime vet-fee cover for a single cat or dog, rated by species, breed, age, region, vet-fee limit and excess.",
  "summary": "Rates annual lifetime vet-fee cover for one cat or dog in the UK. Base rates by species and annual vet-fee limit are adjusted for age band, breed, region, voluntary excess and neutering, then 12% Insurance Premium Tax is added. Older pets and breed/species mismatches are declined; high-limit cover on high-risk breeds is referred."
}
03debug.timer
Step timings

One entry per step, in run order. time is when the step started and elapsed how long it took, both in milliseconds from the start of the run. This whole quote took 4.2 ms.

Use it to find a slow step, usually a very large table or an external call, before it shows up as latency at the aggregators.

input
0.88 ms
base_rates
0.70 ms
age_bands
0.17 ms
breed_factors
0.12 ms
region_factors
0.03 ms
excess_factors
0.23 ms
neuter_factors
0.04 ms
risk_premium
1.39 ms
ipt
0.39 ms
total_premium
0.19 ms
monthly_premium
0.13 ms
pet_exclusions
0.18 ms
pet_refers
0.07 ms
output
0.31 ms
04 · debug.steps

The step trace

One entry per step, keyed by step id, in run order. Each has output (the values it added), plus items showing how it got there and errors for anything it couldn't work out. Values flow down: every step sees the outputs of the steps above it.

StepTypeOutput
inputinputspecies: "dog" · breed: "french_bulldog" · pet_age: 2 · region: "south_east" · vet_fee_limit: 7000 · excess: 100 · neutered: true
base_ratescollectionbase_premium: 298
age_bandscollectionpet_age_factor: 1
breed_factorscollectionbreed_factor: 1.75 · breed_matches: 1
region_factorscollectionregion_factor: 1.12
excess_factorscollectionexcess_factor: 1
neuter_factorscollectionneuter_factor: 0.95
risk_premiumcalculationrisk_premium: 554.88
iptcalculationipt: 66.59
total_premiumcalculationtotal_premium: 621.47
monthly_premiumcalculationmonthly_premium: 51.79
pet_exclusionsexclusionpet_exclusions: false
pet_refersreferpet_refers: true
outputoutputrisk_premium: 554.88 · ipt: 66.59 · monthly_premium: 51.79 · base_premium: 298 · refer: true · result: 621.47 · valid: true
4ainput
How the input was read

One item per input field: the raw value received, the expression that read it and the result the model used. If a field is missing or misspelled in the request, this is where you'll see the default being used instead.

{
  "input": {
    "items": [
      {
        "step": "input",
        "name": "Species",
        "species": "dog",
        "exp": "{{species}}",
        "result": "dog"
      },
      {
        "step": "input",
        "name": "Breed",
        "breed": "french_bulldog",
        "exp": "{{breed}}",
        "result": "french_bulldog"
      }
    ],
    "output": {
      "species": "dog",
      "breed": "french_bulldog",
      "pet_age": 2,
      "region": "south_east",
      "vet_fee_limit": 7000,
      "excess": 100,
      "neutered": true
    }
  }
}
4bcollection
Lookups

Rate-table steps report the values they found in output. Here the French Bulldog row gave a breed factor of 1.75 and the South East a region factor of 1.12. If a lookup finds no matching row, it falls back to its default and says so in errors.

{
  "breed_factors": {
    "errors": [],
    "items": [],
    "output": {
      "breed_factor": 1.75,
      "breed_matches": 1
    }
  },
  "region_factors": {
    "errors": [],
    "items": [],
    "output": {
      "region_factor": 1.12
    }
  }
}
4ccalculation
Formulas and their values

Each calculation item shows the formula, the value of every variable it referenced and the result. You can redo the arithmetic by hand: 298 × 1 × 1.75 × 1.12 × 1 × 0.95 = 554.88. This is the fastest way to find which factor is off.

{
  "risk_premium": {
    "errors": [],
    "items": [
      {
        "formula": "round({{base_premium}} * {{pet_age_factor}} * {{breed_factor}} * {{region_factor}} * {{excess_factor}} * {{neuter_factor}}, 2)",
        "result": 554.88,
        "base_premium": 298,
        "pet_age_factor": 1,
        "breed_factor": 1.75,
        "region_factor": 1.12,
        "excess_factor": 1,
        "neuter_factor": 0.95
      }
    ],
    "output": {
      "risk_premium": 554.88
    }
  }
}
4drefer · exclusion
Rule checks

Every rule in an exclusion or refer step is listed with the values its expression saw and its result. The step's output is true if any rule fired. Here the £7,000 limit and the breed together fired the refer.

{
  "pet_refers": {
    "errors": [],
    "items": [
      {
        "step": "pet_refers",
        "name": "£7,000 limit on a high-risk breed",
        "vet_fee_limit": 7000,
        "breed": "french_bulldog",
        "exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
        "result": true
      }
    ],
    "output": {
      "pet_refers": true
    }
  }
}
05debug.exclusions · debug.refers
Rules that fired

These lists show only the rules that returned true appear here, collected across every rule step in the model. An empty exclusions list means the quote is valid; anything in refers needs an underwriter before binding.

Show these to an underwriter as the reasons for a referral or decline. Each entry carries the rule's name and the values that triggered it.

{
  "exclusions": [],
  "refers": [
    {
      "step": "pet_refers",
      "name": "£7,000 limit on a high-risk breed",
      "vet_fee_limit": 7000,
      "breed": "french_bulldog",
      "exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
      "result": true
    }
  ]
}
06 · debug.errors

Warnings

Empty in our example, which is what you want. When something can't be worked out, the engine doesn't stop: it uses the default and records a warning. Each one names the step, the expression or formula, the inputs it saw and the default_to value it used instead.

Low · a lookup found no row

The same model, quoted for a cat with a Labrador's breed. No row matches, so the breed factor fell back to 1. The model expects this because a separate exclusion declines the quote, but in most models a low warning means a missing row in a rate table.

{
  "step": "breed_factors",
  "level": "low",
  "message": "breed_factor returned empty result for: [{\"query\":\"species\",\"value\":\"cat\"},{\"query\":\"breed\",\"value\":\"labrador\"}]",
  "default_to": 1,
  "expression": "collection.filter(species=={{species}}).filter(breed=={{breed}}).map(breed_rel).first()",
  "inputs": [
    {
      "key": "species",
      "value": "cat"
    },
    {
      "key": "breed",
      "value": "labrador"
    }
  ]
}

And the exclusion that declined it, with valid: false on the result:

[
  {
    "step": "pet_exclusions",
    "name": "Breed does not match species",
    "breed_matches": 0,
    "exp": "{{breed_matches}} == 0",
    "result": true
  }
]
Medium · an expression couldn't be evaluated

From an early draft of this model, where a calculation tested a true/false field directly. The formula couldn't be evaluated, so the neutered factor silently became 1 and every neutered pet missed its 5% discount. The warning was the only sign. The fix was to move the discount into a lookup.

{
  "step": "neuter_factor",
  "level": "medium",
  "message": "neutered is not a number",
  "default_to": 1,
  "formula": "{{neutered}} == true ? 0.95 : 1",
  "inputs": [
    {
      "key": "neutered",
      "value": null
    }
  ]
}
low
A lookup returned nothing and used its default.
medium
A formula or expression failed and used its default.
default_to
The value the engine used instead. If it's part of a multiplication, check it's 1, not 0.
07 · The rest

Excesses, endorsements, indicators

The pet model doesn't use these, so they come back empty. They are always present, so your integration can rely on the shape.

debug.excesses
Excesses that apply
The excess rules that matched, from the model's excess steps, for example a higher excess for a young driver.
debug.endorsements
Wording to add
Endorsement labels that apply to this quote, ready to print on the policy schedule.
debug.indicators
Searchable fields
Values from fields marked index in the model, stored on the quote so you can search your quote history by them.
In practice

Finding a wrong price

These steps work the same whether you, an underwriter or an AI agent is debugging.

01
Start at the answer
Find the step in debug.steps that produced the number that looks wrong. Its output is what every later step used.
02
Walk backwards
Each calculation item lists the value of every variable it used. Go back one step at a time until you find the first value that differs from what you expect. That step is the bug.
03
Check errors first
A default_to in debug.errors means the engine used a default value. A lookup that fell back to 1.0 or 0 is the most common cause of a wrong price.
04
Read the rule items
If a quote is declined or referred unexpectedly, the rule's item shows the exact values its expression saw.
05
Pin a test
Once it's fixed, add the quote's input as a test with the expected price so the same mistake can't come back.