Systems Library / Industry Applications / How to Create Automated Legal Document Assembly
Industry Applications legal

How to Create Automated Legal Document Assembly

Assemble legal documents automatically from clause libraries and data.

Jay Banlasan

Jay Banlasan

The AI Systems Guy

Automated legal document assembly pulls clauses from a library, fills in client data, and produces complete documents in minutes. I built this for a firm that was copying and pasting between Word documents to assemble custom agreements. One wrong paste and a clause from the wrong client ends up in the wrong contract. This system eliminates that risk.

What You Need Before Starting

Step 1: Build the Clause Library

import sqlite3

def init_clause_db(db_path="clause_library.db"):
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS clauses (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            category TEXT,
            name TEXT,
            version TEXT DEFAULT 'standard',
            text TEXT,
            variables TEXT,
            notes TEXT
        )
    """)
    conn.commit()
    conn.close()

def seed_clauses(db_path="clause_library.db"):
    clauses = [
        ("confidentiality", "standard_nda", "standard",
         "Both parties agree to maintain the confidentiality of all proprietary information disclosed during the term of this Agreement and for a period of {{conf_years}} years thereafter.",
         "conf_years", "Standard mutual NDA clause"),
        ("confidentiality", "one_way_nda", "standard",
         "{{receiving_party}} agrees to maintain the confidentiality of all proprietary information disclosed by {{disclosing_party}} for a period of {{conf_years}} years.",
         "receiving_party,disclosing_party,conf_years", "One-way NDA"),
        ("termination", "mutual_termination", "standard",
         "Either party may terminate this Agreement upon {{notice_days}} days written notice to the other party.",
         "notice_days", "Standard mutual termination"),
        ("termination", "for_cause", "standard",
         "Either party may terminate this Agreement immediately upon written notice if the other party materially breaches any provision and fails to cure such breach within {{cure_days}} days.",
         "cure_days", "Termination for cause with cure period"),
        ("liability", "limitation_of_liability", "standard",
         "In no event shall either party's total liability exceed {{liability_cap}}.",
         "liability_cap", "Standard liability cap"),
        ("indemnification", "mutual_indemnification", "standard",
         "Each party shall indemnify and hold harmless the other party from any claims arising from the indemnifying party's breach of this Agreement or negligent acts.",
         "", "Mutual indemnification"),
    ]
    
    conn = sqlite3.connect(db_path)
    for c in clauses:
        conn.execute("INSERT INTO clauses (category, name, version, text, variables, notes) VALUES (?,?,?,?,?,?)", c)
    conn.commit()
    conn.close()

Step 2: Build the Document Assembler

import re

def get_clauses_by_category(category, version="standard", db_path="clause_library.db"):
    conn = sqlite3.connect(db_path)
    rows = conn.execute(
        "SELECT name, text, variables FROM clauses WHERE category = ? AND version = ?",
        (category, version)
    ).fetchall()
    conn.close()
    return [{"name": r[0], "text": r[1], "variables": r[2].split(",") if r[2] else []} for r in rows]

def assemble_document(doc_config, variables):
    sections = []
    section_number = 1
    
    for section in doc_config["sections"]:
        conn = sqlite3.connect("clause_library.db")
        clause = conn.execute(
            "SELECT text FROM clauses WHERE name = ? AND version = ?",
            (section["clause"], section.get("version", "standard"))
        ).fetchone()
        conn.close()
        
        if clause:
            text = clause[0]
            for key, value in variables.items():
                text = text.replace(f"{{{{{key}}}}}", str(value))
            
            heading = section.get("heading", section["clause"].replace("_", " ").title())
            sections.append(f"{section_number}. {heading.upper()}\n{text}")
            section_number += 1
    
    header = doc_config.get("header", "AGREEMENT")
    preamble = doc_config.get("preamble", "").format(**variables) if doc_config.get("preamble") else ""
    
    document = f"{header}\n\n{preamble}\n\n" + "\n\n".join(sections)
    
    missing = re.findall(r"\{\{(\w+)\}\}", document)
    
    return {"document": document, "missing_variables": missing}

Step 3: Define Document Templates

DOCUMENT_CONFIGS = {
    "service_agreement": {
        "header": "SERVICE AGREEMENT",
        "preamble": "This Agreement is entered into as of {effective_date} between {provider_name} (\"Provider\") and {client_name} (\"Client\").",
        "sections": [
            {"clause": "standard_nda", "heading": "Confidentiality"},
            {"clause": "mutual_termination", "heading": "Termination"},
            {"clause": "for_cause", "heading": "Termination for Cause"},
            {"clause": "limitation_of_liability", "heading": "Limitation of Liability"},
            {"clause": "mutual_indemnification", "heading": "Indemnification"},
        ]
    },
    "nda": {
        "header": "NON-DISCLOSURE AGREEMENT",
        "preamble": "This NDA is entered into as of {effective_date} between {party_a} and {party_b}.",
        "sections": [
            {"clause": "standard_nda", "heading": "Confidentiality Obligations"},
        ]
    }
}

Step 4: AI-Powered Clause Customization

import anthropic
from dotenv import load_dotenv
load_dotenv()

def customize_clause(clause_text, customization_request):
    client = anthropic.Anthropic()
    
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        system="""Modify the legal clause as requested. Keep legal precision. Maintain the same structure and formality. Preserve any {{variable}} placeholders. Only change what is specifically requested.""",
        messages=[{
            "role": "user",
            "content": f"Original clause:\n{clause_text}\n\nRequested change:\n{customization_request}"
        }]
    )
    
    return response.content[0].text

Step 5: Full Assembly Workflow

def build_document(doc_type, variables):
    config = DOCUMENT_CONFIGS.get(doc_type)
    if not config:
        return {"error": f"Unknown document type: {doc_type}"}
    
    result = assemble_document(config, variables)
    
    if result["missing_variables"]:
        return {"error": f"Missing: {', '.join(result['missing_variables'])}"}
    
    output_file = f"assembled/{doc_type}_{variables.get('client_name', 'draft').replace(' ', '_')}.txt"
    import os
    os.makedirs("assembled", exist_ok=True)
    with open(output_file, "w") as f:
        f.write(result["document"])
    
    return {"document": result["document"], "file": output_file}

if __name__ == "__main__":
    result = build_document("service_agreement", {
        "effective_date": "January 1, 2026",
        "provider_name": "ABC Consulting LLC",
        "client_name": "XYZ Corporation",
        "conf_years": "3",
        "notice_days": "30",
        "cure_days": "15",
        "liability_cap": "$100,000"
    })
    print(result.get("document", result.get("error")))

What to Build Next

Add clause comparison. When assembling a document for a repeat client, highlight any clauses that differ from their previous agreement. That catches unintentional deviations from negotiated terms.

Related Reading

Want this system built for your business?

Get a free assessment. We will map every system your business needs and show you the ROI.

Get Your Free Assessment

Related Systems