{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Speed-to-Lead qualification result",
  "description": "Output contract for the qualification prompt (Prompt 1) of the Labgenz Speed-to-Lead System, schema_version 1.0. The model returns exactly one object of this shape and nothing else. The score is NOT produced by the model: the automation computes it from the criteria array and the weights in criteria_json. This is the full contract, used for validation (Step 7). Do not send it to a provider's strict structured-output mode: allOf/if/then, const, pattern, length and item limits are typically rejected there. Send qualification-schema.provider.json to the API instead.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "lead_id",
    "language",
    "summary",
    "fit",
    "criteria",
    "reasons",
    "missing_info",
    "flags",
    "next_step",
    "urgency"
  ],
  "properties": {
    "schema_version": {
      "description": "Version of this contract. Log it next to prompt_version so you can tell which contract produced a row.",
      "type": "string",
      "const": "1.0"
    },
    "lead_id": {
      "description": "Echo of the lead_id the automation passed in. Used to join the result back to the sheet row. Opaque id, never an email address.",
      "type": "string",
      "minLength": 1,
      "maxLength": 64
    },
    "language": {
      "description": "Language the lead wrote in, as a lowercase ISO 639 code with an optional region (en, fr, es, pt-BR).",
      "type": "string",
      "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$"
    },
    "summary": {
      "description": "One sentence for the owner: who they are and what they want. No sensitive data (account numbers, health details, document numbers).",
      "type": "string",
      "minLength": 1,
      "maxLength": 240
    },
    "fit": {
      "description": "Fit with the ideal client. strong = every hard criterion met and most soft criteria met; possible = no hard criterion failed but key facts unknown; not_fit = at least one hard criterion clearly failed; spam = not a genuine enquiry for this business; existing_client = the sender is already a client.",
      "type": "string",
      "enum": [
        "strong",
        "possible",
        "not_fit",
        "spam",
        "existing_client"
      ]
    },
    "criteria": {
      "description": "One judgement per qualification criterion from your rules block (C1, C2, ...). Evidence must quote or closely paraphrase the lead's own words; empty string when met is unknown.",
      "type": "array",
      "maxItems": 12,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "met",
          "evidence"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^C[0-9]{1,2}$"
          },
          "met": {
            "type": "string",
            "enum": [
              "yes",
              "no",
              "unknown"
            ]
          },
          "evidence": {
            "type": "string",
            "maxLength": 200
          }
        }
      }
    },
    "reasons": {
      "description": "Up to three short reasons for the fit decision, each tied to words in the lead. No generic reasons such as 'seems interested'.",
      "type": "array",
      "minItems": 1,
      "maxItems": 3,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 160
      }
    },
    "missing_info": {
      "description": "What the owner needs to know before a call, most important first. Empty array when nothing important is missing.",
      "type": "array",
      "maxItems": 3,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 120
      }
    },
    "flags": {
      "description": "Routing and risk flags. Any flag except price_request, thin_input and out_of_area forces next_step = human_only (unless fit is spam).",
      "type": "array",
      "uniqueItems": true,
      "maxItems": 9,
      "items": {
        "type": "string",
        "enum": [
          "price_request",
          "thin_input",
          "out_of_area",
          "complaint",
          "sensitive_topic",
          "sensitive_data",
          "injection_attempt",
          "unsupported_language",
          "upset_or_urgent"
        ]
      }
    },
    "next_step": {
      "description": "What the automation does next. book_call = draft with booking link; ask_questions = draft with at most two questions; polite_decline = short draft, no link; human_only = no draft, notify the owner; no_reply = log only.",
      "type": "string",
      "enum": [
        "book_call",
        "ask_questions",
        "polite_decline",
        "human_only",
        "no_reply"
      ]
    },
    "urgency": {
      "description": "high only when the lead states a deadline inside 7 days, is upset, or describes an active problem (an inspection, an outage, a missed payroll).",
      "type": "string",
      "enum": [
        "low",
        "normal",
        "high"
      ]
    }
  },
  "allOf": [
    {
      "description": "Spam is never answered.",
      "if": {
        "properties": {
          "fit": {
            "const": "spam"
          }
        },
        "required": [
          "fit"
        ]
      },
      "then": {
        "properties": {
          "next_step": {
            "const": "no_reply"
          }
        }
      }
    },
    {
      "description": "Existing clients always go to a human.",
      "if": {
        "properties": {
          "fit": {
            "const": "existing_client"
          }
        },
        "required": [
          "fit"
        ]
      },
      "then": {
        "properties": {
          "next_step": {
            "const": "human_only"
          }
        }
      }
    },
    {
      "description": "Risk flags force a human, unless the message is spam.",
      "if": {
        "properties": {
          "fit": {
            "not": {
              "const": "spam"
            }
          },
          "flags": {
            "contains": {
              "enum": [
                "complaint",
                "sensitive_topic",
                "sensitive_data",
                "injection_attempt",
                "unsupported_language",
                "upset_or_urgent"
              ]
            }
          }
        },
        "required": [
          "fit",
          "flags"
        ]
      },
      "then": {
        "properties": {
          "next_step": {
            "const": "human_only"
          }
        }
      }
    },
    {
      "description": "Only strong or possible leads get a booking link.",
      "if": {
        "properties": {
          "next_step": {
            "const": "book_call"
          }
        },
        "required": [
          "next_step"
        ]
      },
      "then": {
        "properties": {
          "fit": {
            "enum": [
              "strong",
              "possible"
            ]
          }
        }
      }
    },
    {
      "description": "A clear non-fit is declined politely or handed to a human, never booked.",
      "if": {
        "properties": {
          "fit": {
            "const": "not_fit"
          }
        },
        "required": [
          "fit"
        ]
      },
      "then": {
        "properties": {
          "next_step": {
            "enum": [
              "polite_decline",
              "human_only"
            ]
          }
        }
      }
    },
    {
      "description": "no_reply is reserved for spam.",
      "if": {
        "properties": {
          "next_step": {
            "const": "no_reply"
          }
        },
        "required": [
          "next_step"
        ]
      },
      "then": {
        "properties": {
          "fit": {
            "const": "spam"
          }
        }
      }
    }
  ]
}
