Developer · Quote debugging

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

One trace for everyone

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

For AI
Simple for agents to follow
Steps keep the ids they have in the model and every formula lists the values it used. An agent can trace back to the first wrong number and name the step and row to fix
For people
Reads like a worksheet
Rules keep their plain names and formulas show their numbers, so an underwriter can redo a premium by hand or see exactly why a quote referred
For both
Nothing hidden
It's all in the response, with no separate logs to dig through. You see what the agent saw, so you can check its diagnosis before anything changes in 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 trace shows why

Quote input
{
  "species": "dog",
  "breed": "french_bulldog",
  "pet_age": 2,
  "region": "south_east",
  "vet_fee_limit": 7000,
  "excess": 100,
  "neutered": true
}
Response with debug=true, trace folded
{
  "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
      }
    }
  }
}
Detail view of a saved quote for a French Bulldog on a 7,000 limit: result 621.47 with additional outputs, the raised refer, and the full quote JSON
01result · valid · output

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
  }
}
02debug.meta

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."
}
03debug.timer

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

input
0.050 ms
base_rates
0.025 ms
age_bands
0.027 ms
breed_factors
0.038 ms
region_factors
0.014 ms
excess_factors
0.012 ms
neuter_factors
0.012 ms
risk_premium
0.047 ms
ipt
0.023 ms
total_premium
0.025 ms
monthly_premium
0.024 ms
pet_exclusions
0.027 ms
pet_refers
0.021 ms
output
0.070 ms
04 · debug.steps

The 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

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
Build tab showing the model as a chain of steps, with the French Bulldog test scenario run alongside: result 621.47, valid, with one refer raised
4ainput

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
    }
  }
}
4bcollection

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
    }
  }
}
4ccalculation

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
    }
  }
}
4drefer · exclusion

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
    }
  }
}
05debug.exclusions · debug.refers

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
    }
  ]
}
06 · debug.errors

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

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 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
  }
]
Medium · a formula couldn't be worked out

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
07 · The rest

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

debug.excesses
Excesses that apply
The excess rules that matched in the model's excess steps, such as a higher excess for a young driver
debug.endorsements
Wordings to add
The endorsement codes that apply to this quote, ready for the policy schedule. See Policies
debug.indicators
Searchable fields
Values the model marks for indexing, saved with the quote so you can search your quote history by them
Quote history

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

Project Data tab for UK Pet Insurance: Cats and Dogs, with quotes, sales and conversion trends, an hourly chart of public quotes, and a table of recent quotes with their version, result, validity and date
In practice

Tracking down a wrong price

These steps work the same whether it's you, an underwriter or an AI agent doing the digging

01
Start at the answer
Find the step in debug.steps that made the number that looks wrong. Its output is what every later step used
02
Work backwards
Each calculation lists the value of every variable it used. Step back one at a time until a value isn't what you expect. That step is the bug
03
Check the warnings
A default_to in debug.errors means the engine fell back to a default. A lookup that dropped to 1 or 0 is the most common cause of a wrong price
04
Read the rules
If a quote declines or refers when it shouldn't, the rule's entry shows exactly what its expression saw
05
Add a test
Once it's fixed, save the quote's input as a test with the right price so the same mistake can't come back