Free lesson · Forward Deployed GenAI Engineering
Detect risky contract language with NeMo Guardrails
You build a ContractRiskAnalyzer that uses NeMo Guardrails colang flows to flag unlimited revisions, missing SLAs, and liability gaps in SOW text and suggest safer alternatives.
Course: AI Solution Delivery · Chapter 3 · SOW & Proposal Generation
Free to read — no subscription required.
Introduction
When teams circulate SOW drafts containing phrases like "unlimited revisions" or "to the client's satisfaction," they unknowingly accept liabilities that no pricing calculator can quantify — and reviewing every clause manually across dozens of sections is slow and inconsistent. Automated guardrails can intercept that language at drafting time, before terms are locked and sent to a client. By the end of this lesson, you'll be able to build a contract risk analyzer powered by NeMo Guardrails that detects the five major risk categories — unlimited scope, ambiguous acceptance, missing SLAs, liability gaps, and scope boundary gaps — and suggests corrective language in place of each flagged clause.
Key Terminology
- NeMo Guardrails — NVIDIA's framework, specifically the
LLMRailsclass, that sits in front of an underlying language model and intercepts every incoming message before generation; used here to route full SOW text through risk-detection flows so risky clauses are caught at drafting time rather than after the model responds. RailsConfig— The NeMo class instantiated viaRailsConfig.from_path("./config/contract_rails")that reads all Colang flow definitions from a local directory and packages them into a configuration object passed toLLMRails.- Colang flow — A named declarative rule, stored in the
./config/contract_railsdirectory, that matches substring patterns in$last_user_messageand fires abot flag riskaction when a risky clause is detected; each of the five risk categories has its own flow (e.g.,unlimited_scope_detection,ambiguous_acceptance_detection). bot flag riskaction — The Colang directive that labels a matched clause with a risk category identifier (such asUNLIMITED_SCOPEorMISSING_SLA) and a severity level (criticalorhigh), always paired with abot suggestdirective that provides corrective replacement language.- Contract risk category — One of five distinct classes of problematic SOW language the analyzer targets: unlimited scope, ambiguous acceptance, missing SLAs, liability gaps, and scope boundary gaps — each triggering its own Colang flow when its characteristic phrases appear in a clause.
- Severity tagging — The practice of labeling each flagged risk with a priority level (
criticalfor unlimited scope,highfor ambiguous acceptance and missing SLAs) so downstream tooling or reviewers can triage which clauses require the most urgent correction.
Concepts
Interception vs. Post-Hoc Review
The most important architectural decision in this lesson is when risk detection happens. A traditional approach — scanning a finalized SOW after it has been drafted — still leaves the risky language in the document until a human notices the flag and manually reworks the clause. NeMo Guardrails takes a different position: LLMRails intercepts the contract text before the underlying model processes it, meaning the analysis runs at input time. The practical consequence is that every clause of the SOW passes through the detection layer as part of the call to analyze(), and flagged output — including replacement suggestions — is returned in the same response rather than as a separate review step.
This interception model is what makes automated guardrails useful at drafting time. The analyst does not need to remember to run a separate linter; the risk check is embedded in the generation pipeline itself.
Colang Flows as Declarative Pattern Rules
The detection logic is not embedded in Python — it lives in Colang flow definitions stored in the ./config/contract_rails directory. Each flow is a self-contained rule: it specifies what to watch for (substring patterns in $last_user_message) and what to do (fire bot flag risk with a category name and severity, then emit a bot suggest replacement). Separating the rules from the ContractRiskAnalyzer class means new risk patterns can be added by writing a new Colang flow without touching the Python wrapper at all.
The three flows shown in the walkthrough — unlimited_scope_detection, ambiguous_acceptance_detection, and missing_sla_detection — illustrate how pattern specificity varies by category. The unlimited-scope flow matches explicit unbounded-commitment phrases ("unlimited revision", "as many iterations"). The ambiguous-acceptance flow targets subjective quality language ("to the client's satisfaction", "industry standard"). The missing-SLA flow uses a two-condition check: the word "availability" must appear without a numeric threshold like "99" — a structural pattern rather than a phrase match. The liability gap and scope boundary gap flows follow the same if … then bot flag risk … bot suggest shape (see Code Walkthrough).
Risk Categories as a Coverage Map
The five risk categories define the full surface area the analyzer is designed to cover. Unlimited scope and ambiguous acceptance are the most common sources of runaway delivery commitments — they remove the boundary around what "done" means. Missing SLAs create enforcement gaps when a client later claims the system underperformed a threshold that was never written down. Liability gaps and scope boundary gaps are structural omissions: the absence of a limitation-of-liability clause or an explicit exclusion section is itself the risk, not any specific phrase. Together the five categories form a coverage map: a SOW that passes all five flows contains no clause patterns that belong to any of these known risk classes.
Because each bot flag risk action is paired with a bot suggest replacement, the analyzer's output is actionable rather than diagnostic-only. A critical flag on UNLIMITED_SCOPE arrives alongside a concrete rewrite — capped revision cycles billed at the change-order rate — giving the proposal author a drop-in correction rather than a vague warning.
Code Walkthrough
Now that you understand the five contract risk categories, the implementation shows how NeMo Guardrails translates each pattern into a programmatic detection rail that intercepts risky language before a proposal reaches a client.
The ContractRiskAnalyzer class wraps NeMo's LLMRails to route every clause of a SOW draft through a guardrails configuration. The constructor loads rails from a local ./config/contract_rails directory; the analyze method sends the full contract text as a user message and returns the guardrails-filtered response as a string containing any flagged risks and replacement suggestions.
Code snippetpython
1from nemoguardrails import RailsConfig, LLMRails 2 3class ContractRiskAnalyzer: 4 """Scans contract text for risky language patterns.""" 5 6 def __init__(self): 7 config = RailsConfig.from_path("./config/contract_rails") 8 self.rails = LLMRails(config) 9 10 async def analyze(self, contract_text: str) -> str: 11 """Send contract text through guardrails and return flagged risk output.""" 12 return await self.rails.generate_async( 13 messages=[{ 14 "role": "user", 15 "content": f"Analyze this contract for risks:\n\n{contract_text}", 16 }] 17 )
The detection logic lives in Colang flow definitions stored inside the ./config/contract_rails directory. Each flow matches $last_user_message against the language patterns from the five risk categories and fires a severity-tagged bot flag risk action when a match is found. The three flows below cover the first three categories from the Concepts section — unlimited scope, ambiguous acceptance, and missing SLA:
Code snippetcolang
1define flow unlimited_scope_detection 2 user said something 3 if "unlimited revision" in $last_user_message 4 or "as many iterations" in $last_user_message 5 or "until satisfied" in $last_user_message 6 then 7 bot flag risk "UNLIMITED_SCOPE" severity "critical" 8 bot suggest "Replace with: Up to [N] revision cycles per milestone. Additional revisions billed at the change order rate." 9 10define flow ambiguous_acceptance_detection 11 user said something 12 if "to the client's satisfaction" in $last_user_message 13 or "reasonably acceptable" in $last_user_message 14 or "industry standard" in $last_user_message 15 then 16 bot flag risk "AMBIGUOUS_ACCEPTANCE" severity "high" 17 bot suggest "Replace with specific, measurable criteria referencing the acceptance criteria appendix." 18 19define flow missing_sla_detection 20 user said something 21 if "availability" in $last_user_message 22 and "99" not in $last_user_message 23 then 24 bot flag risk "MISSING_SLA" severity "high" 25 bot suggest "Add explicit SLA: 99.X% uptime measured monthly, excluding planned maintenance windows."
The flows for liability gap and scope boundary gaps follow the same structure — a conditional match on the pattern phrases from the Concepts section paired with a bot flag risk action at the appropriate severity. Because LLMRails intercepts each message before it reaches the underlying model, risky language is flagged at analysis time rather than after a contract has been signed and priced.
Confirm that passing a clause containing "unlimited revisions until the client is satisfied" to ContractRiskAnalyzer().analyze() produces output that references UNLIMITED_SCOPE at critical severity and includes the capped-revision replacement language.
Do's and Don'ts
Having walked through the material above, the following Do's and Don'ts distill it into practice.
Do's
- ✓Do define each of the five risk categories as a separate named Colang flow — keeping
unlimited_scope_detection,ambiguous_acceptance_detection,missing_sla_detection, and their siblings as independent flows lets you extend, tune, or disable one category without touching the others; collapsing all five into a single compound conditional makes the pattern logic unmaintainable and risks silently dropping a whole category when a branch is miswired. - ✓Do pair every
bot flag riskaction with abot suggestaction that supplies concrete replacement language — flaggingUNLIMITED_SCOPEat critical severity is only useful if the analyzer also outputs the capped-revision substitute clause; a flag without a suggestion leaves drafters knowing that a clause is risky but not how to fix it before the proposal goes to a client. - ✓Do call
rails.generate_async()and deliver contract text as a"user"role message —LLMRailsroutes the message through all registered flows before the underlying model is invoked, which is what ensures risky language like "unlimited revisions until the client is satisfied" is intercepted and severity-tagged at analysis time rather than after a contract has been priced and sent.
Don'ts
- ✗Don't implement pattern detection in Python string operations instead of Colang flow definitions — Python-side matching runs outside the
LLMRailsinterception layer, sobot flag riskandbot suggestactions are never triggered, the guardrails configuration in./config/contract_railsis effectively bypassed, and risky clauses pass through silently. - ✗Don't invert the negation logic in absence-detection flows —
missing_sla_detectionfires when"availability"is present AND"99"is not present; reversing thatnot incondition causes the flow to flag compliant, SLA-bearing clauses as risky and wave through every contract section that genuinely omits an uptime commitment. - ✗Don't pass a file path to
RailsConfig.from_path()instead of the./config/contract_railsdirectory — the loader expects a directory containing both the YAML model configuration and the Colang flow files as siblings; pointing it at a single.coor.yamlfile raises a load error at construction time and preventsContractRiskAnalyzer.__init__from completing.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the Forward Deployed GenAI Engineering subscription.
From · cancel anytime
More free lessons in AI Solution Delivery
- Ch 1Generate executive discovery reports from structured assessment data
- Ch 2Classify project risks with DSPy-optimized prompts
- Ch 3Generate full SOW proposals with LangGraph workflows
- Ch 3Detect risky contract language with NeMo GuardrailsYou are here
- Ch 4Build a RAG prototype with pgvector retrieval
- Ch 4Package prototypes with Dockerfiles, Helm charts, and K8s manifests
- Ch 5Detect and redact PII with Presidio and LlamaGuard 4