
See how every price was made
Ask for a debug trace and Swallow returns every input it read, every lookup, every formula with its values, every rule it checked and how long each step took. It's plain JSON, like the models. Here's a real one, step by step
Made for AI. Clear to people.
People and AI agents read the same trace. When a price looks off, you both see the same steps, values and warnings
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 trace shows why
{
"species": "dog",
"breed": "french_bulldog",
"pet_age": 2,
"region": "south_east",
"vet_fee_limit": 7000,
"excess": 100,
"neutered": true
}{
"quote_reference": "<uuid>",
"project_reference": "<uuid>",
"version_reference": "<uuid>",
"version": 1,
"result": 621.47,
"valid": true,
"refers": "[ 1 entry ]",
"exclusions": [],
"excesses": [],
"endorsements": [],
"output": {
"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": "{ 14 steps }"
}
}Show the full trace (4.9 KB)
{
"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.05,
"key": "input"
},
{
"time": 0.025,
"elapsed": 0.025,
"key": "base_rates"
},
{
"time": 0.052,
"elapsed": 0.027,
"key": "age_bands"
},
{
"time": 0.09,
"elapsed": 0.038,
"key": "breed_factors"
},
{
"time": 0.104,
"elapsed": 0.014,
"key": "region_factors"
},
{
"time": 0.116,
"elapsed": 0.012,
"key": "excess_factors"
},
{
"time": 0.128,
"elapsed": 0.012,
"key": "neuter_factors"
},
{
"time": 0.176,
"elapsed": 0.047,
"key": "risk_premium"
},
{
"time": 0.199,
"elapsed": 0.023,
"key": "ipt"
},
{
"time": 0.223,
"elapsed": 0.025,
"key": "total_premium"
},
{
"time": 0.248,
"elapsed": 0.024,
"key": "monthly_premium"
},
{
"time": 0.275,
"elapsed": 0.027,
"key": "pet_exclusions"
},
{
"time": 0.296,
"elapsed": 0.021,
"key": "pet_refers"
},
{
"time": 0.366,
"elapsed": 0.07,
"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
}
}
}
}result · valid · outputThe price, whether it can be bound and every field the model returnsdebug.metaThe model that priced the quotedebug.timerHow long each step took, in millisecondsdebug.stepsThe full trace: every input, lookup, formula and rule, with its valuesdebug.exclusions · refersOnly the rules that fired, and the values behind themdebug.errorsWarnings where the engine fell back to a default. Empty is goodexcesses · endorsements · indicatorsExcesses and wordings that apply, and the fields kept for search
The result
result is the price from the model's result formula, here the annual premium with IPT. valid turns false when any exclusion fires. A declined quote still has a price, but it mustn't be bound
output holds the model's own fields: the premium split, the monthly figure and refer, which is true here. These are the numbers to show a customer or print on a schedule
{
"result": 621.47,
"valid": true,
"output": {
"risk_premium": 554.88,
"ipt": 66.59,
"monthly_premium": 51.79,
"base_premium": 298,
"refer": true,
"result": 621.47,
"valid": true
}
}The model that priced it
The model's name, description and summary at the moment it priced the quote. With the version and version_reference on the response, every price traces back to an exact model, so “what was live when this was quoted?” always has an answer
{
"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."
}Step timings
One entry per step, in the order they ran. time is when a step started and elapsed how long it took, both in milliseconds from the start. The engine priced this whole quote in 0.44 ms
Use it to spot a slow step, usually a very large table or an external call, before it shows up as latency on a price comparison site
inputbase_ratesage_bandsbreed_factorsregion_factorsexcess_factorsneuter_factorsrisk_premiumipttotal_premiummonthly_premiumpet_exclusionspet_refersoutputThe step trace
One entry per step, keyed by its id, in run order. Each has an output with the values it added, items showing how it got there and errors for anything it couldn't work out. Values flow down, so every step sees the outputs above it
| Step | Type | Output |
|---|---|---|
input | input | species: "dog" · breed: "french_bulldog" · pet_age: 2 · region: "south_east" · vet_fee_limit: 7000 · excess: 100 · neutered: true |
base_rates | collection | base_premium: 298 |
age_bands | collection | pet_age_factor: 1 |
breed_factors | collection | breed_factor: 1.75 · breed_matches: 1 |
region_factors | collection | region_factor: 1.12 |
excess_factors | collection | excess_factor: 1 |
neuter_factors | collection | neuter_factor: 0.95 |
risk_premium | calculation | risk_premium: 554.88 |
ipt | calculation | ipt: 66.59 |
total_premium | calculation | total_premium: 621.47 |
monthly_premium | calculation | monthly_premium: 51.79 |
pet_exclusions | exclusion | pet_exclusions: false |
pet_refers | refer | pet_refers: true |
output | output | risk_premium: 554.88 · ipt: 66.59 · monthly_premium: 51.79 · base_premium: 298 · refer: true · result: 621.47 · valid: true |

Reading the input
One item per input field: the value that arrived, the expression that read it and the result the model went on with. If a field is missing or misspelt in the request, this is where you'll see the default step in
{
"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
}
}
}Lookups
Rate table steps put what they found in output. Here the French Bulldog row gives a breed factor of 1.75 and the South East a region factor of 1.12. When no row matches, a lookup 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
}
}
}Formulas and their values
Every calculation item shows its formula, the value of each variable it used and the result, so you can check the sum yourself: 298 × 1 × 1.75 × 1.12 × 1 × 0.95 = 554.88. It's the quickest way to find the factor that's 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
}
}
}Rule checks
Exclusion and refer steps list every rule they checked, 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 set off 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
}
}
}Rules that fired
These lists only hold rules that returned true, gathered from every rule step in the model. An empty exclusions list means the quote is valid. Anything in refers needs an underwriter before it's bound
Each entry carries the rule's name and the values that set it off, ready to show an underwriter as the reason for a refer or decline
{
"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
}
]
}Warnings
Empty in our example, which is what you want. When something can't be worked out the engine keeps going: it uses the default and logs a warning. Each one names the step, the expression or formula, the inputs it saw and the default_to value it used
The same model, quoted for a cat with a Labrador's breed. No row matches, so the breed factor falls back to 1. That's expected here, because an exclusion declines the quote anyway. In most models a low warning means a row is missing from 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
}
]The same model with the neutered discount written as a calculation on a true/false field. The formula can't be evaluated, so the factor quietly becomes 1, the neutered Persian misses its 5% discount and the price moves to 268.78. The warning is the only clue. The fix is the lookup the model uses now
{
"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 found nothing and used its default
medium- A formula or expression failed and used its default
default_to- The value used instead. If it feeds a multiplication, make sure it's 1, not 0
Excesses, endorsements, indicators
The pet model doesn't use these, so they come back empty. They're always there, so your integration can count on the shape
Every quote, on the record
Each project's Data tab charts quotes, sales and conversion, and lists recent quotes with their version, result and validity. Open any quote to see its trace, or download them as JSON or CSV

Tracking down a wrong price
These steps work the same whether it's you, an underwriter or an AI agent doing the digging


