# Insurance: A Bundle Without The Shape Behind The Login

**Published:** 2026-11-10  
**Author:** Kin Lane  
**Canonical:** https://apievangelist.com/2026/11/10/insurance-a-bundle-without-the-shape-behind-the-login/

I've been building bundles that pair a JSON Schema with OpenLint-format rules, after finding out that [the decoder only pours part of the bedrock](https://apievangelist.com/2026/11/02/the-decoder-only-pours-part-of-the-bedrock/). Insurance is the awkward one, and that makes it interesting. When I [looked at this industry](https://apievangelist.com/2026/10/23/insurance-the-shape-of-the-answer/) I concluded that the shape is real and it's behind a login. So what can you bundle when you can't read the standard?

## The answer I picked

Turn a claimant's email into a first notice of loss against one policy. The first thing to say is what this is not. **This isn't ACORD.** I don't have the NGDS files, they're for members, and I haven't read them. The six base fields come from our own placeholder in the [ACORD catalog profile](https://apis.io/providers/acord/), which I already told you has an API Evangelist `generated` marker and is a stand-in for the real shape. If you hold ACORD membership, you could do this properly, and your field names would replace mine.

Here's what I find interesting, though. A lot of what makes this bundle useful doesn't come from the standard at all. It comes from the policy.

```yaml
bundle:
  id: insurance-first-notice-of-loss
  version: 0.1.0
  status: illustrative            # NOT ACORD. The NGDS files are members only and were not read.
  answers: Turn a claimant's email into a first notice of loss against one policy
  derived_from:
    - API Evangelist's own six-field placeholder in the ACORD catalog profile
    - The policy record supplied with each request
engine:
  runtime: deepinfer
  tested_against: {xgrammar: 0.2.2, strict_mode: true}
  so_args: {schema_type: json}
  sampling: {temperature: 0, max_tokens: 1024, seq_seed: 20261110}
  env: {DEEPINFER_BATCH_INVARIANT: 1, DEEPINFER_ENABLE_MTP_VARLEN: 0}
input:
  context: [email, receivedOn, policy]
  rules:
    policy-has-a-start-date:
      description: The policy period is what the output rules check against
      given: $.context.policy
      severity: error
      then: {field: effective, function: truthy}
    policy-carries-coverages:
      description: The coverage list becomes the enum, so it cannot be empty
      given: $.context.policy.coverages
      severity: error
      then: {function: length, functionOptions: {min: 1}}
shape:
  type: object
  additionalProperties: false
  required: [policyId, lossDate, lossDescription, lossLocation, claimant, coverage, evidence]
  properties:
    policyId: {const: "${context.policy.id}"}
    lossDate: {type: string, format: date}
    lossDescription: {type: string, minLength: 10, maxLength: 1000}
    lossLocation: {type: string, minLength: 3, maxLength: 200}
    claimant: {type: string, minLength: 2, maxLength: 140}
    coverage: {enum: "${context.policy.coverages}"}
    estimatedLossAmount: {type: string, pattern: "^[0-9]{1,10}\\.[0-9]{2}$"}
    evidence:
      type: array
      minItems: 1
      items: {type: string}
    # No reserve field. The decoder cannot emit what the shape does not name.
output:
  rules:
    loss-inside-policy-period:
      description: The loss cannot predate the policy
      given: $
      severity: error
      then:
        function: notAfter
        functionOptions: {earlier: context.policy.effective, later: answer.lossDate}
    loss-before-the-policy-ends:
      description: The loss cannot postdate the policy
      given: $
      severity: error
      then:
        function: notAfter
        functionOptions: {earlier: answer.lossDate, later: context.policy.expires}
    loss-before-the-email:
      description: A loss cannot be reported before it happens
      given: $
      severity: error
      then:
        function: notAfter
        functionOptions: {earlier: answer.lossDate, later: context.receivedOn}
    facts-come-from-the-email:
      description: Every cited passage must appear in the email
      given: $
      severity: error
      then:
        function: quotedInSource
        functionOptions: {quotes: answer.evidence, source: context.email}
    estimate-is-the-claimants:
      description: An estimate is a claim someone made, flag it for the adjuster
      given: $.answer
      severity: warn
      then: {field: estimatedLossAmount, function: undefined}
on_failure:
  error: {retry: 2, change: [seq_seed, append_rule_messages_to_prompt], then: send_to_adjuster}
  warn: {action: flag_for_review}
  always: {action: adjuster_sets_the_reserve}
```

## Where the design choices are

**The real enum is on the policy.** When I wrote the first post, I said the coverage should be one the policy actually carries, and not a plausible string. An industry code list can tell you which coverages exist. Only this policy can say which ones this insured bought. So `coverage` binds from `context.policy.coverages`, per request, and the decoder can't reach a coverage that isn't on the policy. No standard would have given me this, even a public one. The two are complementary, and the standard's job is naming things consistently across carriers.

**There's no reserve field.** In the first post I said the agentic access layer is where you'd declare that setting a reserve needs a human. In a bundle you can do something stronger and simpler. Leave it out. With `additionalProperties: false`, a decoder can't emit a field the shape doesn't name. A reserve hits financial statements. A model should get no way to write one.

**Dates get checked against three things.** The loss has to fall inside the policy period, and it can't come after the email reporting it. All three comparisons need the envelope, because none of those facts is in the answer. A decoder can tell you it's a date. It can't tell you it's a date this policy covers.

**The estimate gets flagged, not rejected.** If a claimant says the damage is about $40,000, capturing that is useful, but it's a claim someone made. `estimate-is-the-claimants` fires as a `warn` whenever it's present, so the adjuster sees it as the claimant's number.

## How it runs as part of inference

1. **Input rules run first.** No coverages on the policy record or no start date, and the request stops. An empty coverage list would otherwise be an empty enum, and that's a compiler error.
2. **The policy is bound into the shape and compiled**, in 414 milliseconds on my laptop against DeepSeek's real tokenizer, the slowest in this series so far. These are first-compile numbers and DeepInfer caches compiled grammars, but with a per-policy shape the cache has less to work with, as the bound shape can differ on each request. I didn't measure what that does at volume, and after a catastrophe volume is the whole problem.
3. **The decoder generates.** The policy ID is fixed, the coverage is from the policy, and no reserve exists.
4. **Output rules run on the email and the answer together.**
5. **Failures retry twice and go to an adjuster, who sets the reserve either way.**

## What the fixtures showed

These are hand-written documents testing each path, not model output.

- A burst pipe email became a clean notice and passed everything.
- A notice dated January 2027, describing a fire when the email describes a burst pipe, citing a quote that isn't in the email, **was accepted by the decoder**. The rules flagged the quote, the date after the email, and the date after the policy expired.
- A notice with a `flood` coverage the policy doesn't carry, plus an `initialReserve` of 50,000, was refused by the decoder. Both failures come from what the shape simply doesn't allow.
- A policy record with no coverages and no start date was stopped by the input rules.

Now the failure that matters most in this industry. The fixture picks `dwelling` for a burst pipe, and I think that's reasonable, but my rules have no opinion. If the model picks `liability`, also on the policy, every check passes. Choosing the right coverage is a coverage determination, and that's a judgment regulators and courts review. A bundle narrows the choice to what's on the policy. It doesn't make that choice right, and I wouldn't want anybody reading this to think it could.

## What's still missing

The obvious gap is the standard. Carriers using this pattern with their own field names would be recreating the problem I described in the first post, where everybody writes their own claim schema. In fact this bundle is an example of it, which is uncomfortable to say about my own file. What would change that is what I asked for last time: ACORD publishing the claim, coverage, party and submission core as open JSON Schema and vocabularies. Everything here would then bind to shared names, and a carrier's work would shrink to the policy context and the rules.

There are also pieces I left out. Claimant matching against a party record already on file needs identity data that I didn't want in an example. Multi-claimant losses and notices from brokers would break my single `claimant` string. And a free-text location is a weak answer next to a verified address. Scanned claim forms need extraction before they ever reach this step.

Insurance taught me something I didn't expect going in. I assumed that a bundle without the standard would be hollow. What I found is that the most protective parts of this file come from the policy record and from a field I left out. The shape behind the login still matters for making it work across the industry. For keeping a model out of trouble on one claim, you already hold most of what you need.
