Fill forms from email
Fill a form from an email. Fields it cannot find stay empty and are flagged.
Claims, bookings and applications still arrive as emails that someone retypes into a form. This guide fills the whole form with one Sage2 call, typed and ready to save.
The useful part is what it doesn't do: a field that isn't in the email stays empty, and a field Sage2 isn't sure of is flagged for a person.
- Extract
- Read every field of the claim formExtract
- Policyholder
- Daniel Okoro
- Policy number
- AU-77120-K
- Phone
- 07700 900123
- Date of incident
- 2026-10-16
- Time of incident
- 18:40
- Vehicle
- Aurora C5
- Registration
- AB12 CDE
- Damage
- rear bumper, boot lid
- Repair estimate
- £2,340.00
- Police report number
- missing
- Your codeClaim form filled
The application saves 9 fields and drafts the reply: “Thanks, Daniel. Could you send us the police report number?”
One Extract request read all 10 fields: 96 ms.
Get the code
Set SAGE_API_KEY (get a free key), then run it. Python: pip install requests. TypeScript: Node 22+. Save as recipe.mts and run npx tsx recipe.mts.
A null answer is not an error. Sage2 isn't sure, and each step below says what to do with it. Limits, errors and retries: Limits and errors.
import os
import requests
def sage(path, body):
r = requests.post(
"https://sage.levanto.ai" + path,
headers={"Authorization": f"Bearer {os.environ['SAGE_API_KEY']}"},
json=body,
timeout=30,
)
r.raise_for_status()
return r.json()
FORM = {
"policyholder": "the person making the claim",
"policy_number": "the insurance policy number",
"phone": "the phone number of the person making the claim",
"incident_date": {"type": "date", "definition": "the date the accident happened"},
"incident_time": {"type": "time", "definition": "the time the accident happened"},
"vehicle": "the make and model of the policyholder's car",
"registration": "the policyholder's car registration",
"damage": ["a part of the policyholder's car that was damaged"],
"repair_estimate": {"type": "money", "definition": "the quoted cost of the repair"},
"police_report": "the police report or crime reference number",
}
email = """Hi,
I need to report an accident. My name is Daniel Okoro and my policy number is AU-77120-K.
On Friday 16 October 2026, at around 6:40 pm, I was stopped at the lights on Baker Street when a white van went into the back of my car. It's an Aurora C5, registration AB12 CDE.
The van driver was Sam Whitfield. Nobody was hurt, thankfully, but the rear bumper and the boot lid are smashed. The garage quoted £2,340 to fix it.
A police officer came but I didn't get a report number. They said they would send it by post.
Thanks,
Daniel
07700 900123"""
results = sage("/v1/extract", {"content": email, "extract": FORM})["results"]
for field, r in results.items():
if "values" in r: # a field with every mention
sure = all((v["probability"] or 0) >= 0.6 for v in r["values"])
print(field, [v["value"] for v in r["values"]], "" if sure else "-> check")
elif r["value"] is None:
print(field, "-> ask the customer" if r["probability"] is not None else "-> check: too close to call")
else:
print(field, r["value"], "" if r["probability"] >= 0.6 else "-> check")
How it works
Each form field becomes a field in
extract: a name and a short definition. Add atypewhere your form needs one:incident_datehas typedate,incident_timehas typetime.repair_estimatehas typemoney. It comes back as{amount, currency}.damageis a list. You get every damaged part.
Runs with the helper from Get the code.
FORM = { "policyholder": "the person making the claim", "policy_number": "the insurance policy number", "phone": "the phone number of the person making the claim", "incident_date": {"type": "date", "definition": "the date the accident happened"}, "incident_time": {"type": "time", "definition": "the time the accident happened"}, "vehicle": "the make and model of the policyholder's car", "registration": "the policyholder's car registration", "damage": ["a part of the policyholder's car that was damaged"], "repair_estimate": {"type": "money", "definition": "the quoted cost of the repair"}, "police_report": "the police report or crime reference number", }Send the email to
/v1/extractand readresults. Each field gives the value, the exacttextit came from and aprobability.Runs with the helper from Get the code.
email = """Hi, I need to report an accident. My name is Daniel Okoro and my policy number is AU-77120-K. On Friday 16 October 2026, at around 6:40 pm, I was stopped at the lights on Baker Street when a white van went into the back of my car. It's an Aurora C5, registration AB12 CDE. The van driver was Sam Whitfield. Nobody was hurt, thankfully, but the rear bumper and the boot lid are smashed. The garage quoted £2,340 to fix it. A police officer came but I didn't get a report number. They said they would send it by post. Thanks, Daniel 07700 900123""" results = sage("/v1/extract", {"content": email, "extract": FORM})["results"]Example Sage2 output
{ "values": { "policyholder": "Daniel Okoro", "policy_number": "AU-77120-K", "phone": "07700 900123", "incident_date": "2026-10-16", "incident_time": "18:40", "vehicle": "Aurora C5", "registration": "AB12 CDE", "damage": [ "rear bumper", "boot lid" ], "repair_estimate": { "amount": 2340, "currency": "GBP" }, "police_report": null }, "results": { "policyholder": { "value": "Daniel Okoro", "text": "Daniel Okoro", "start": 46, "end": 58, "probability": 0.9328, "probabilities": [ { "value": "Daniel Okoro", "text": "Daniel Okoro", "start": 46, "end": 58, "probability": 0.9328 }, { "value": "Daniel", "text": "Daniel", "start": 529, "end": 535, "probability": 0.055 } ] }, "policy_number": { "value": "AU-77120-K", "text": "AU-77120-K", "start": 83, "end": 93, "probability": 0.9941, "probabilities": [ { "value": "AU-77120-K", "text": "AU-77120-K", "start": 83, "end": 93, "probability": 0.9941 }, { "value": null, "probability": 0.0059 } ] }, "…": "one entry per field" }, "meta": { "model": "levanto-sage-2", "usage": { "input_tokens": 458, "output_tokens": 921 }, "latency_ms": 96.2 } }- Your code
Fill the form and flag the gaps
Each field lands in one of three cases:
- Found:
valueis filled andprobabilitysays how likely it is. Below your bar (0.6 here), flag it for a check. A list field likedamagehas aprobabilityper item. - Not in the email:
valueisnullandprobabilityis how likely it's absent. Ask the customer for it. - Too close to call:
valueandprobabilityare bothnull. A person checks it.
This email has no police report number. The reply asks for it.
Runs with the helper from Get the code.
for field, r in results.items(): if "values" in r: # a field with every mention sure = all((v["probability"] or 0) >= 0.6 for v in r["values"]) print(field, [v["value"] for v in r["values"]], "" if sure else "-> check") elif r["value"] is None: print(field, "-> ask the customer" if r["probability"] is not None else "-> check: too close to call") else: print(field, r["value"], "" if r["probability"] >= 0.6 else "-> check") - Found:
Before you ship
- Check it's a claim first. An email asking "does my policy cover chips?" still fills the form. Ask a Yes/No first ("Is this a new claim?") and fill only on yes.
- Watch partial values and close calls. A date with no year comes back as
--11-04. When the email has two candidates,probabilitieslists both: flag the field when the runner-up is close. - Show where each value came from.
text,startandendpoint at the exact words. A reviewer can check each field against them. - Set a bar per field. A wrong policy number costs more than a misspelled car model.
- Fast and small. One call fills this form in about 0.15 s. It reads about 460 tokens and returns about 900.
- Many emails at once?
/v1/extract/batchtakes up to 64contentswith oneextract.