> ## Documentation Index
> Fetch the complete documentation index at: https://developer.box.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an NDA risk summary with Box AI Extract

> Extract key clauses from an NDA or MSA with Box AI structured extraction, turn them into a Deal Risk Summary as JSON and markdown, and save the summary back to Box as a companion file.

export const RelatedLinks = ({title, items = []}) => {
  const getBadgeClass = badge => {
    if (!badge) return "badge-default";
    const badgeType = badge.toLowerCase().replace(/\s+/g, "-");
    return `badge-${badge === "ガイド" ? "guide" : badgeType}`;
  };
  if (!items || items.length === 0) {
    return null;
  }
  return <div className="my-8">
      {}
      <h3 className="text-sm font-bold uppercase tracking-wider mb-4">{title}</h3>

      {}
      <div className="flex flex-col gap-3">
        {items.map((item, index) => <a key={index} href={item.href} className="py-2 px-3 rounded related_link hover:bg-[#f2f2f2] dark:hover:bg-[#111827] flex items-center gap-3 group no-underline hover:no-underline border-b-0">
            {}
            <span className={`px-2 py-1 rounded-full text-xs font-semibold uppercase tracking-wide flex-shrink-0 ${getBadgeClass(item.badge)}`}>
              {item.badge}
            </span>

            {}
            <span className="text-base">{item.label}</span>
          </a>)}
      </div>
    </div>;
};

export const Link = ({href, children, className, ...props}) => {
  const localizedHref = localizeLink(href);
  return <a href={localizedHref} className={className} {...props}>
      {children}
    </a>;
};

Legal teams review the same handful of clauses in every non-disclosure agreement (NDA) and master service agreement (MSA): what law governs it, how long it lasts, how either side can walk away, how long confidentiality survives, and whether liability is capped. Reading each document by hand is slow, and asking a model open-ended questions ("What are the risky terms in this contract?") returns different answers every time.

This tutorial takes a more structured approach: **extract, don't prompt.** You define a fixed extraction schema, so Box AI returns the same clauses in the same shape for every document. You then turn that predictable output into a *Deal Risk Summary* and save it back to Box as a companion file that reviewers can open next to the original.

## What you are building

By the end of this tutorial, you have a working Python project that:

* Authenticates to Box and reads an NDA stored in a Box folder.
* Defines a clause-extraction schema for governing law, term, termination, confidentiality period, and liability cap.
* Calls the [POST /2.0/ai/extract\_structured](/reference/post-ai-extract-structured) endpoint with confidence scores and source references enabled.
* Applies a simple, transparent rule set to score each clause and assemble a Deal Risk Summary in JSON and markdown.
* Uploads the summary back to Box as a companion file, so the analysis lives alongside the source document.

<Note>
  This tutorial focuses on an NDA, but the same schema and workflow apply to a master service agreement (MSA) or most other commercial contracts. Adjust the fields in `schema.py` to match the clauses you care about.
</Note>

## Why extract instead of prompting

A free-form prompt asks the model to decide *what* to return and *how* to format it, so the response shape drifts between documents and runs. Structured extraction inverts that: you declare the fields, and Box AI fills them in.

| Approach                                                  | Output                                               | Best for                                                 |
| --------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------- |
| Ad-hoc prompt (`POST /2.0/ai/ask`)                        | Free-form text that varies per run                   | Exploratory questions, summaries for a human to read     |
| Structured extraction (`POST /2.0/ai/extract_structured`) | A consistent, typed JSON object keyed by your fields | Repeatable pipelines, scoring, and downstream automation |

Because the output is predictable, you can score it with plain code and trust that the summary looks the same for every contract. Enabling **confidence scores** and **source references** on the extraction call also lets you flag clauses that need a human's eyes.

<Warning>
  The risk scoring in this tutorial is illustrative and intended for triage only. It is not legal advice, and it does not replace review by a qualified professional. Treat the summary as a starting point that routes attention, not as a decision.
</Warning>

## Prerequisites

Before you start, make sure you have the following:

* A free <Link href="https://account.box.com/signup/developer#ty9l3">Box developer account</Link>, which gives you access to the Box AI API.
* A Box application configured with **Client Credentials Grant** authentication.
* Python 3.11 or higher.
* An NDA uploaded to Box. You can download a <Link href="/static/quickstarts/extract/files/nda-sample.pdf">sample NDA</Link> and upload it to a Box folder. Note its **file ID** from the URL. For example, if the URL is `https://app.box.com/file/123456789`, the file ID is `123456789`. Because a Client Credentials Grant app authenticates as a separate service account, add that service account as a collaborator with the **Editor** role on the file (or its parent folder) so it can read the NDA. You find the service account email in the **Authenticate the Box client** step.
* A Box folder where the service account can write the summary, with the same service account added as an **Editor** collaborator. Note its **folder ID** from the URL.
* The following scopes enabled on your app:
  * Read and write all files and folders stored in Box
  * Manage AI

## Step-by-step process

This tutorial uses two Box Platform capabilities:

| Component          | Purpose                                                                      | API                                                 |
| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------- |
| **Box AI Extract** | Pull the five clauses as typed fields, with confidence scores and references | `POST /2.0/ai/extract_structured`                   |
| **Uploads**        | Save the generated summary back to Box as a companion file                   | `POST https://upload.box.com/api/2.0/files/content` |

This tutorial uses the default Extract Agent to keep the setup simple, since you don't configure an agent. Confidence scoring is available either way: the default Extract Agent runs on Gemini 2.5 Flash and the [Enhanced Extract Agent](/guides/box-ai/ai-tutorials/extract-metadata-structured#enhanced-extract-agent) runs on Gemini 2.5 Pro, and both are [confidence-capable Gemini models](/guides/metadata/confidence-level#model-support). If your NDAs have complex or scanned layouts, see [Scaling to production](#scaling-to-production) for how to switch to the Enhanced Extract Agent without losing confidence scores.

<Tip>
  Prefer not to build the project step by step? Clone the <Link href="https://github.com/box-community/box-nda-risk-summary">sample code</Link> for this tutorial and run it directly.
</Tip>

<Steps>
  <Step title="Set up the development environment">
    1. Open your terminal and create a new project directory:

    ```bash theme={null}
    mkdir nda-risk-summary && cd nda-risk-summary
    ```

    2. Create and activate a Python virtual environment:

    ```bash theme={null}
    python3 -m venv .venv
    source .venv/bin/activate
    ```

    After activation, your terminal prompt shows `(.venv)` at the beginning. This confirms you are working inside the virtual environment.

    <Note>
      Every time you open a new terminal window or tab, re-activate the virtual environment by running `source .venv/bin/activate` from the project directory. If you see `ModuleNotFoundError` when running commands, it usually means the venv is not activated.
    </Note>

    3. Install the required packages:

    ```bash theme={null}
    pip install boxsdk python-dotenv
    ```

    <Note>
      Installing `boxsdk` gives you the `box_sdk_gen` module used throughout this tutorial. Confidence scores and source references are available directly on the SDK's `create_ai_extract_structured` method through the `include_confidence_score` and `include_reference` parameters.
    </Note>

    4. Create a `.env` file to store your credentials, then add the following content. Replace the placeholder values with your actual credentials from the [Box Developer Console](https://app.box.com/developers/console):

    ```bash theme={null}
    BOX_CLIENT_ID=your_client_id
    BOX_CLIENT_SECRET=your_client_secret
    BOX_ENTERPRISE_ID=your_enterprise_id
    NDA_FILE_ID=your_file_id
    OUTPUT_FOLDER_ID=your_folder_id
    ```

    <Warning>
      Never commit `.env` files to version control. Add `.env` to your `.gitignore`.
    </Warning>
  </Step>

  <Step title="Authenticate the Box client">
    In your project, create a file called `box_client.py` and add the following code. It builds an authenticated client that you use to call Box AI and the uploads API through the SDK.

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import BoxClient, BoxCCGAuth, CCGConfig

    load_dotenv()

    def get_box_client() -> BoxClient:
        config = CCGConfig(
            client_id=os.getenv("BOX_CLIENT_ID"),
            client_secret=os.getenv("BOX_CLIENT_SECRET"),
            enterprise_id=os.getenv("BOX_ENTERPRISE_ID"),
        )
        auth = BoxCCGAuth(config=config)
        return BoxClient(auth=auth)
    ```

    <Warning>
      A CCG application acts as a separate service account user that does not automatically have access to your content. Invite the service account email (found in the [Developer Console](https://app.box.com/developers/console) under **General Settings**) as a collaborator with the **Editor** role on both the folder that contains your NDA and the output folder. Editor access is required because the app writes the summary file back to Box. Without access, API calls return `404 Not Found`.
    </Warning>
  </Step>

  <Step title="Define the clause schema">
    Create a file called `schema.py` and add the following code. Each field tells Box AI exactly what to look for. The `prompt` on each field adds context that improves accuracy on dense legal language. The fields are `CreateAiExtractStructuredFields` objects that you pass directly to the SDK's extraction call in the next step.

    ```python theme={null}
    from box_sdk_gen import CreateAiExtractStructuredFields

    EXTRACTION_FIELDS = [
        CreateAiExtractStructuredFields(
            key="governing_law",
            display_name="Governing law",
            type="string",
            prompt="The governing law or jurisdiction that applies to the agreement.",
        ),
        CreateAiExtractStructuredFields(
            key="term",
            display_name="Term",
            type="string",
            prompt="The overall duration of the agreement, including any renewal terms.",
        ),
        CreateAiExtractStructuredFields(
            key="termination",
            display_name="Termination",
            type="string",
            prompt="The termination conditions, including any required notice period.",
        ),
        CreateAiExtractStructuredFields(
            key="confidentiality_period",
            display_name="Confidentiality period",
            type="string",
            prompt="How long confidentiality obligations remain in effect after termination.",
        ),
        CreateAiExtractStructuredFields(
            key="liability_cap",
            display_name="Liability cap",
            type="string",
            prompt="The limitation of liability or liability cap, including any amount and currency.",
        ),
    ]
    ```

    <Tip>
      A clear, specific `prompt` and `description` improve both the extracted value and its confidence score. See <Link href="/guides/metadata/confidence-level">Metadata confidence level</Link> for how confidence is calculated.
    </Tip>
  </Step>

  <Step title="Extract the clauses">
    Create a file called `extract.py` and add the following code. It uses the SDK's `create_ai_extract_structured` method to send the schema to Box AI and asks for confidence scores and source references alongside the extracted values.

    ```python theme={null}
    from box_sdk_gen import AiItemBase

    from box_client import get_box_client
    from schema import EXTRACTION_FIELDS

    def extract_clauses(file_id: str) -> dict:
        client = get_box_client()

        response = client.ai.create_ai_extract_structured(
            items=[AiItemBase(id=file_id)],
            fields=EXTRACTION_FIELDS,
            include_confidence_score=True,
            include_reference=True,
        )

        return {
            "answer": response.answer or {},
            "confidence_score": response.confidence_score or {},
            "reference": response.reference or {},
        }
    ```

    The response includes three parts you use in the next step:

    * `answer` holds the extracted value for each field.
    * `confidence_score` holds a `level` (`LOW`, `MEDIUM`, or `HIGH`) and a numeric `score` between 0 and 1 for each field.
    * `reference` holds the source text and its location in the document for each field.

    <Note>
      `reference` returns the exact snippet and location Box AI used for each field. Its contents are passed straight through into your summary file in a later step so a reviewer can trace every value back to the source. For background, see the <Link href="/changelog">Reference in extract</Link> changelog entry.
    </Note>
  </Step>

  <Step title="Score risk and build the summary">
    Create a file called `risk.py` and add the following code. It applies a small, transparent rule set to each clause, assigns a severity, and assembles the Deal Risk Summary as a dictionary. It also renders a markdown version for humans.

    ```python theme={null}
    CLAUSES = [
        "governing_law",
        "term",
        "termination",
        "confidentiality_period",
        "liability_cap",
    ]

    APPROVED_JURISDICTIONS = ["delaware", "new york", "california", "england"]

    SEVERITY_ORDER = {"none": 0, "low": 1, "medium": 2, "high": 3}

    def assess_clause(key: str, value, confidence: dict):
        # A missing clause is the highest risk: it could not be located in the document.
        if not value:
            return "high", "Clause not found. Review the document manually."

        severity, note = "low", "Clause found."

        if key == "liability_cap" and any(
            term in value.lower() for term in ["unlimited", "no cap", "not limited"]
        ):
            severity, note = "high", "Liability appears to be uncapped."
        elif key == "governing_law" and not any(
            j in value.lower() for j in APPROVED_JURISDICTIONS
        ):
            severity, note = "medium", "Governing law is outside the approved list."

        # A low-confidence extraction is never treated as better than medium risk.
        score = (confidence or {}).get("score", 1)
        if score < 0.7 and SEVERITY_ORDER[severity] < SEVERITY_ORDER["medium"]:
            severity = "medium"
            note = f"{note} Low extraction confidence ({score:.2f}); verify against the source."

        return severity, note

    def build_risk_summary(extraction: dict, document: str) -> dict:
        answer = extraction["answer"]
        scores = extraction["confidence_score"]
        references = extraction["reference"]

        findings = []
        for key in CLAUSES:
            value = answer.get(key)
            confidence = scores.get(key)
            severity, note = assess_clause(key, value, confidence)
            findings.append({
                "clause": key,
                "value": value,
                "severity": severity,
                "note": note,
                "confidence": confidence,
                "source": references.get(key),
            })

        overall = max(findings, key=lambda f: SEVERITY_ORDER[f["severity"]])["severity"]
        return {"document": document, "overall_risk": overall, "findings": findings}

    def to_markdown(summary: dict) -> str:
        lines = [
            f"# Deal Risk Summary: {summary['document']}",
            "",
            f"**Overall risk:** {summary['overall_risk'].upper()}",
            "",
            "> Generated automatically for triage only. This is not legal advice.",
            "",
            "| Clause | Value | Risk | Confidence | Notes |",
            "| --- | --- | --- | --- | --- |",
        ]
        for f in summary["findings"]:
            level = f["confidence"]["level"] if f["confidence"] else "N/A"
            value = f["value"] or "_Not found_"
            clause = f["clause"].replace("_", " ").title()
            lines.append(
                f"| {clause} | {value} | {f['severity'].upper()} | {level} | {f['note']} |"
            )
        return "\n".join(lines)
    ```

    <Tip>
      Keep the rules in `assess_clause` obvious and easy to edit. Adjust `APPROVED_JURISDICTIONS`, the confidence threshold, or the clause-specific checks to match your organization's playbook.
    </Tip>
  </Step>

  <Step title="Save the summary back to Box">
    Create a file called `app.py` and add the following code. It runs the extraction, builds the summary, and uploads both the JSON and markdown versions to your output folder as companion files.

    ```python theme={null}
    import io
    import json
    import os

    from dotenv import load_dotenv
    from box_sdk_gen import UploadFileAttributes, UploadFileAttributesParentField

    from box_client import get_box_client
    from extract import extract_clauses
    from risk import build_risk_summary, to_markdown

    load_dotenv()

    def upload_companion_file(client, folder_id: str, name: str, content: str) -> str:
        files = client.uploads.upload_file(
            attributes=UploadFileAttributes(
                name=name,
                parent=UploadFileAttributesParentField(id=folder_id),
            ),
            file=io.BytesIO(content.encode("utf-8")),
        )
        return files.entries[0].id

    if __name__ == "__main__":
        file_id = os.getenv("NDA_FILE_ID")
        folder_id = os.getenv("OUTPUT_FOLDER_ID")

        extraction = extract_clauses(file_id)
        print("Raw extraction:")
        print(json.dumps(extraction, indent=2))

        summary = build_risk_summary(extraction, document=f"NDA-{file_id}")
        markdown = to_markdown(summary)

        print("\nDeal Risk Summary:")
        print(markdown)

        client = get_box_client()
        json_id = upload_companion_file(
            client, folder_id, f"{summary['document']}-risk-summary.json",
            json.dumps(summary, indent=2),
        )
        md_id = upload_companion_file(
            client, folder_id, f"{summary['document']}-risk-summary.md", markdown,
        )

        print(f"\nUploaded JSON summary (file ID {json_id}) and markdown summary (file ID {md_id}).")
    ```

    At this point, your project directory should contain the following files:

    ```
    nda-risk-summary/
    ├── .env
    ├── .venv/
    ├── app.py
    ├── box_client.py
    ├── extract.py
    ├── risk.py
    └── schema.py
    ```

    <Note>
      Uploading the summary as a new, separate file keeps the original NDA untouched. If you would rather version the source document itself, use `client.uploads.upload_file_version(file_id, ...)` instead, but note that replacing a PDF with a markdown or JSON body changes the file's type.
    </Note>
  </Step>

  <Step title="Run the extraction">
    Make sure you are in the `nda-risk-summary` directory with the virtual environment activated, then run the script:

    ```bash theme={null}
    python3 app.py
    ```

    Box AI returns the extracted clauses together with a confidence score for each field. Your raw extraction looks similar to this (the `reference` object is omitted here for brevity):

    ```json theme={null}
    {
      "answer": {
        "governing_law": "State of New York",
        "term": "Two years from the Effective Date",
        "termination": "Either party may terminate with 30 days written notice.",
        "confidentiality_period": "Three years after termination",
        "liability_cap": "Fees paid in the 12 months preceding the claim"
      },
      "confidence_score": {
        "governing_law": { "level": "HIGH", "score": 0.95 },
        "term": { "level": "HIGH", "score": 0.92 },
        "termination": { "level": "MEDIUM", "score": 0.81 },
        "confidentiality_period": { "level": "MEDIUM", "score": 0.78 },
        "liability_cap": { "level": "LOW", "score": 0.64 }
      }
    }
    ```

    The script then prints the Deal Risk Summary. Because the liability cap was extracted with low confidence, it is raised to medium risk, which sets the overall risk for the document:

    ```markdown theme={null}
    # Deal Risk Summary: NDA-123456789

    **Overall risk:** MEDIUM

    > Generated automatically for triage only. This is not legal advice.

    | Clause | Value | Risk | Confidence | Notes |
    | --- | --- | --- | --- | --- |
    | Governing Law | State of New York | LOW | HIGH | Clause found. |
    | Term | Two years from the Effective Date | LOW | HIGH | Clause found. |
    | Termination | Either party may terminate with 30 days written notice. | LOW | MEDIUM | Clause found. |
    | Confidentiality Period | Three years after termination | LOW | MEDIUM | Clause found. |
    | Liability Cap | Fees paid in the 12 months preceding the claim | MEDIUM | LOW | Clause found. Low extraction confidence (0.64); verify against the source. |
    ```

    Finally, open your output folder in Box. You should see two new files: `NDA-123456789-risk-summary.json` and `NDA-123456789-risk-summary.md`, sitting alongside your other content and ready for review.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="ModuleNotFoundError: No module named '...'">
    Your virtual environment is not activated. Run `source .venv/bin/activate` from the project directory before running any `python3` commands. Each new terminal tab needs its own activation.
  </Accordion>

  <Accordion title="invalid_client: The client credentials are invalid">
    Check your `.env` file:

    * Verify `BOX_CLIENT_ID` and `BOX_CLIENT_SECRET` match the values in Developer Console > Configuration.
    * Confirm `BOX_ENTERPRISE_ID` is your enterprise ID.
    * Ensure your app is authorized in the Developer Console and uses Client Credentials Grant.
  </Accordion>

  <Accordion title="404 Not Found">
    The service account does not have access to the file or folder. Invite the service account email (found in Developer Console > General Settings) as a collaborator with the **Editor** role on both the NDA's folder and the output folder.
  </Accordion>

  <Accordion title="confidence_score or reference is empty in the response">
    Confirm your extraction call sets `include_confidence_score` and `include_reference` to `True`, as shown in `extract.py`. Confidence estimation also depends on the model behind the extraction agent - see <Link href="/guides/metadata/confidence-level#model-support">model support</Link>.
  </Accordion>

  <Accordion title="409 Item with the same name already exists">
    `app.py` uploads the summary using a deterministic name derived from the file ID (for example, `NDA-123456789-risk-summary.json`), so running the script again against the same output folder tries to create files that are already there. Delete the previous `-risk-summary.json` and `-risk-summary.md` files from the output folder, or point `OUTPUT_FOLDER_ID` at a different folder, before re-running.
  </Accordion>
</AccordionGroup>

## Scaling to production

<AccordionGroup>
  <Accordion title="Trigger the summary automatically on upload">
    Instead of running the script by hand, register a webhook on your NDA folder so every upload triggers extraction and write-back. See <Link href="/tutorials/invoice-intake">Automate invoice intake with Box AI Extract</Link> for an end-to-end webhook pattern, and <Link href="/guides/webhooks/v2/signatures-v2">Verify webhook signatures</Link> to validate incoming requests.
  </Accordion>

  <Accordion title="Route low-confidence clauses to human review">
    Use the confidence score to build a review queue: automatically accept high-confidence clauses and flag anything below your threshold for a person. See <Link href="/guides/metadata/confidence-level#implementing-human-review-workflows">Implementing human review workflows</Link> for suggested thresholds and patterns.
  </Accordion>

  <Accordion title="Handle complex or scanned documents">
    For multi-page contracts, dense tables, or scanned PDFs, switch to the <Link href="/guides/box-ai/ai-tutorials/extract-metadata-structured#enhanced-extract-agent">Enhanced Extract Agent</Link> for better accuracy. It runs on Gemini 2.5 Pro, which supports confidence scoring, so you keep confidence scores and references when you switch. Add the agent to your extraction call:

    ```python theme={null}
    from box_sdk_gen import AiAgentReference, AiAgentReferenceTypeField

    response = client.ai.create_ai_extract_structured(
        items=[AiItemBase(id=file_id)],
        fields=EXTRACTION_FIELDS,
        include_confidence_score=True,
        include_reference=True,
        ai_agent=AiAgentReference(
            id="enhanced_extract_agent",
            type=AiAgentReferenceTypeField.AI_AGENT_ID,
        ),
    )
    ```

    You can confirm which model processed a request by checking `ai_agent_info.models` in the response.
  </Accordion>

  <Accordion title="Make summaries searchable with metadata">
    In addition to the companion file, write the top-level clause values back to the NDA as a Box <Link href="/guides/metadata/queries">metadata instance</Link>. You can then filter contracts by governing law, overall risk, or confidentiality period across your whole repository.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Supplier agreement extraction" href="/tutorials/supplier-agreements" icon="file-contract" arrow="true">
    Extract grouped and repeating structured data from a contract using the `struct` and `table` field types.
  </Card>

  <Card title="Extract API reference" href="/reference/post-ai-extract-structured" icon="code" arrow="true">
    See the full API specification for structured extraction.
  </Card>
</CardGroup>

<RelatedLinks
  title="RELATED GUIDES"
  items={[
{ label: translate("Extract metadata from file (structured)"), href: "/guides/box-ai/ai-tutorials/extract-metadata-structured", badge: "GUIDE" },
{ label: translate("Metadata confidence level"), href: "/guides/metadata/confidence-level", badge: "GUIDE" },
{ label: translate("Extract APIs overview and use cases"), href: "/guides/box-ai/ai-tutorials/extract-use-cases", badge: "GUIDE" },
{ label: translate("Extract structured data quick start"), href: "/guides/box-ai/quick-start/box-ai-extract", badge: "QUICKSTART" }
]}
/>
