# Custom AI Extractor — Guide

**Audience:** ZantIQ tenants (admins) authoring custom extractors, and internal on-call / support.
**Path in-app:** Settings → **AI Extractors** (`/settings/extractors`).

---

## What this is (and what it isn't)

The Custom Extractor lets you point ZantIQ's AI at **specific clauses** in a contract that the built-in extractors don't already cover — a notice period, a renewal escalator, a data-residency clause, an assignability restriction, etc. — and either enforce them as Commitments (breach detection, rules engine, health score) or land them as passive reference fields on the Contract.

> **This is a contract extractor. It is not a general-purpose LLM playground and it is not a program-scoring tool.** ZantIQ enforces obligations that appear in executed contracts. Do not use the extractor to score your own security program, produce audit-readiness reports, or grade your compliance activities. Vanta / Drata / SecureFrame do that; ZantIQ does contract obligation enforcement. The editor shows this reminder inline every time you author a prompt.

---

## The three platform extractors (read-only)

Before you write anything, know what's already running. The **Platform extractors** section of the AI Extractors page shows all three canned prompts and what fields they produce:

1. **Contract classifier** — reads the first ~3,000 characters and identifies contract type (MSA / SOW / NDA / amendment / other), signatory parties, effective date, term length, total value, currency.
2. **Commitment extractor** — the workhorse. Chunk-by-chunk pass focused on response-time SLAs, uptime guarantees, data retention policies, support tiers, penalty clauses, entitlements, escalation paths, and milestones. Each commitment gets name, type, contract value, verbatim clause, page number, external validating system, confidence, and notes.
3. **Amendment diff** — compares an amendment against the base contract's commitments and returns changed / removed / new / plain-English summary.

These run on every contract you ingest, regardless of whether you have any custom extractors. Custom extractors run **after** them, in the same ingestion pipeline.

---

## Anatomy of a custom extractor

Every custom extractor has:

| Field | What it is |
|---|---|
| **Slug** | URL-safe identifier, unique per tenant. `renewal_terms`, `data_residency_clauses`, etc. Cannot change after creation. |
| **Name** | Human-facing display name. |
| **Description** | One line explaining what this extractor is for. |
| **Prompt template** | Free-form text you write for the LLM. Must contain the `{text}` placeholder — that's where the contract text is substituted at run time. |
| **Output schema** | 1–20 declared fields, each with a name, a type (`string` / `integer` / `number` / `boolean`), a description, and a **landing rule** (see below). |

### Landing rules — the important one

Each output field lands in one of two places:

- **`Enforce as Commitment`** — creates a Commitment row on the contract. Runs through breach detection, the rules engine, and the health score, exactly like SLAs extracted by the platform's built-in prompt. Use this for anything you want to actively track and get alerts on.
- **`Reference on Contract`** — lands in `Contract.extracted_fields` under the key `"<slug>.<field_name>"`. Passive: shown on the Contract detail page but not enforced. Use this for informational values you just want to see (data residency region, notice-period days, etc.).

Pick whichever fits — you can mix both in the same extractor.

---

## Three worked examples

### 1. Renewal terms — mostly reference

```
Slug: renewal_terms
Name: Renewal Terms
Description: Pulls the notice period and renewal type from every contract.

Prompt template:
Extract the auto-renewal handling and notice period from the contract.
Return null for any field not present.

CONTRACT TEXT:
{text}

Output fields:
  notice_period_days   integer   "Days of notice required to prevent auto-renewal"     Reference
  renewal_type         string    "auto | manual | evergreen | none"                    Reference
  renewal_term_months  integer   "Length in months of each renewal cycle"              Reference
```

All three land on `Contract.extracted_fields` — passive metadata for renewal ops to see at a glance.

### 2. Price escalator — mixed enforcement + reference

```
Slug: price_escalator
Name: Annual Price Escalator
Description: Detects the max annual price increase clause.

Prompt template:
Find the clause governing annual price adjustments. Return the maximum
allowed percentage increase and the timing basis.

CONTRACT TEXT:
{text}

Output fields:
  max_annual_increase_pct   number   "Maximum % increase per year"                   Enforce as Commitment
  timing_basis              string   "'anniversary' | 'january' | 'other'"           Reference
```

`max_annual_increase_pct` becomes a Commitment — you get an alert if the vendor bills more than this. `timing_basis` is just informational.

### 3. Data residency — pure enforcement

```
Slug: data_residency
Name: Data Residency Requirements
Description: Extracts any data-residency clauses the customer needs enforced.

Prompt template:
Look for any clauses that restrict where customer data may be stored,
processed, or transmitted. If multiple regions are specified, list them
comma-separated.

CONTRACT TEXT:
{text}

Output fields:
  allowed_regions   string   "Regions where data may reside (comma-separated)"   Enforce as Commitment
```

Any Rule Engine rule you write that references data residency now has a Commitment to fire against.

---

## The lifecycle

1. **Draft** — you create an extractor in draft state. It does not run against any contracts. You can edit prompt + schema freely.
2. **Activate** — moves the extractor to active. It now runs against every subsequent contract ingest (and vendor contract ingest, and the background ingestion pipeline). Existing contracts are **not** back-filled — this is intentional, since older contracts may not match the shape you're expecting.
3. **Pause (move back to draft)** — required before editing an active extractor. Prevents in-flight ingestions from seeing schema-mismatched values.
4. **Archive** — soft-delete. Extractor stops running; existing extracted values on Commitments and Contracts stay put.

### Health signals

Every extractor row shows:
- Last successful run timestamp
- Recent failure count (last 5 runs)
- Last error message

**After 5 consecutive failures, the extractor is auto-moved to draft** and stops running. Look at the last error to figure out what went wrong (usually: prompt template asks for something the LLM can't reliably produce, or the schema declares a field the LLM refuses to return). Fix and re-activate.

---

## Test-on-contract before you commit

The **Test** button on any extractor runs it against a specific contract's stored text and shows a preview table:

- What fields the LLM returned
- The actual extracted values
- What would land where (Commitment vs Contract.extracted_fields)

No Commitments are created and `Contract.extracted_fields` is not written. Use this to iterate on your prompt before setting the extractor to `Active`.

---

## Guardrails (things the platform enforces)

- **Prompt template must contain `{text}`.** If it doesn't, activation is blocked.
- **Output schema:** 1–20 fields per extractor. Field names lowercase snake_case, unique per extractor.
- **Reserved slugs:** `system` and any slug starting with `system:` are blocked (those are used by the platform extractors).
- **Contract text is capped at 80,000 characters** before being sent to the LLM. Most contracts are 20–60k so this rarely bites, but very long agreements are truncated.
- **Auto-JSON preamble:** whatever prompt you write, we wrap it with a system-authored preamble that forces the LLM to return JSON matching your declared schema. Extra fields the LLM produces are dropped before persistence.
- **Auto-disable:** 5 consecutive failures move the extractor to draft. You get a red badge on the row explaining why.
- **Audit:** every test invocation is logged (`custom_extractor.invoked` audit event) with contract id, token counts, and duration. Production runs are tracked via each extractor's `last_success_at` / `failure_count` / `last_error` columns.

---

## Provenance — knowing where a Commitment came from

Every Commitment carries an `extracted_by_extractor_slug` column:

- **`system`** — created by one of the 3 canned platform extractors.
- **`<your_slug>`** — created by one of your custom extractors.
- **NULL** — created by a user in the UI (manual).

The API exposes this on every commitment row; the UI shows a chip on the Commitments list and Contract detail page (shipping in a follow-up polish commit — the data is available today).

---

## Troubleshooting

**"prompt_template must contain the '{text}' placeholder"** — literal. Add `{text}` somewhere in your prompt.

**"slug 'system' and 'system:*' are reserved for platform extractors"** — pick a different slug.

**"only draft extractors can be edited"** — move it back to draft (Pause button on the row), then edit.

**"extractor returned non-JSON"** — the LLM ignored the JSON-preamble. Simplify your prompt template — usually caused by overly leading instructions or contradictions with the system preamble.

**Extractor auto-disabled** — check the red failure banner on the row. Most common causes: schema declares a field the LLM refuses to fill (usually because it's asking for structured data that doesn't exist in the contract), or the prompt is contradictory. Fix the prompt or drop the offending field and re-activate.

**"cannot activate: ..."** — validation runs on activation too, not just save. Whatever the message says is what to fix.

---

## Not supported (deliberately)

- **Framework catalogs (SOC 2 / ISO 27001 / NIST CSF / HIPAA / etc.) shipped as installable data.** Regulatory Template Packs were previously a feature; they were removed 2026-07-18 because they pushed ZantIQ toward GRC-tool territory. If you need to track a framework, the correct approach is: write a Rule Engine rule that fires on a Commitment created by a custom extractor — not to install a canonical framework catalog into the platform.
- **Running an extractor against non-contract text** (uploaded policies, internal docs, questionnaires). The extractor endpoint takes a `contract_id`, not free text.
- **Cross-tenant extractor sharing.** Extractors are tenant-scoped. There's no marketplace or "public extractors" concept — and there won't be, because it's an on-ramp to GRC drift.
- **Editing an active extractor without pausing.** Requires the Pause → Edit → Activate flow. Prevents mid-flight schema mismatches.
