CLIPS Rule Authoring Guide
This guide explains how to write, test, and deploy custom CLIPS rules with the nxusKit SDK.
CLIPS Rule Syntax
Section titled “CLIPS Rule Syntax”nxusKit uses CLIPS 6.4, a forward-chaining inference engine. Rules follow the pattern:
(defrule rule-name "Documentation string" ;; LHS: conditions (pattern matching) (template-name (slot-name ?variable)) (test (< ?variable 100)) => ;; RHS: actions (assertions, side effects) (assert (alert (alert-type "threshold_exceeded") (severity "warning") (message (str-cat "Value " ?variable " is out of range")) (recommendation "Check your input data") (entity-id ?id) (rule-name "rule-name") (module-name "data-qc"))))Defining Templates
Section titled “Defining Templates”Templates define the fact schemas your rules operate on. Define them in shared template files loaded before any module rules:
;;; shared/000-core.clp — Core templates
(deftemplate input-data "A single data record to evaluate" (slot record-id (type INTEGER)) (slot value (type FLOAT)) (slot category (type STRING)) (slot confidence (type FLOAT) (range 0.0 1.0)))
(deftemplate threshold-config "Configurable thresholds for QC checks" (slot value-min (type FLOAT)) (slot value-max (type FLOAT)) (slot confidence-min (type FLOAT) (default 0.5)))
(deftemplate alert "Output: a raised alert from rule inference" (slot alert-type (type STRING)) (slot severity (type STRING)) ;; "critical", "high", "warning", "info" (slot message (type STRING)) (slot recommendation (type STRING)) (slot entity-id (type INTEGER)) (slot rule-name (type STRING)) (slot module-name (type STRING)))Modules
Section titled “Modules”CLIPS modules provide namespace isolation for rules. Each module groups related rules:
;;; In data-qc/bounds-check.clp(defmodule data-qc (export ?ALL))
(defrule bounds-check "Flag records outside configured bounds" (threshold-config (value-min ?vmin) (value-max ?vmax)) (input-data (record-id ?rid) (value ?v&:(or (< ?v ?vmin) (> ?v ?vmax)))) => (assert (alert (alert-type "out_of_bounds") (severity "high") (message (str-cat "Record " ?rid " value " ?v " outside [" ?vmin "-" ?vmax "]")) (recommendation "Verify input data or adjust thresholds") (entity-id ?rid) (rule-name "bounds-check") (module-name "data-qc"))))Directory Structure
Section titled “Directory Structure”Organize rules with shared templates loaded first, then per-module rule files:
rules/ shared/ # Shared templates (loaded first, alphabetically) 000-core.clp # input-data, threshold-config, alert 010-domain.clp # Additional domain-specific templates data-qc/ # Data quality checks bounds-check.clp confidence-check.clp classification/ # Classification rules category-classifier.clp custom/ # User-defined rules my-custom-check.clpCLIPS Integration Paths
Section titled “CLIPS Integration Paths”nxusKit provides two ways to use CLIPS:
- Provider chat — CLIPS as a standard chat provider. Send
ClipsInputJSON as the user message; receiveClipsOutputJSON in the response content. Best for request/response workflows and cross-language portability. - Session API — Direct engine access via
ClipsSession(Rust, Go, Python) or the C ABI (nxuskit_clips_session_*). Best for interactive, multi-step rule authoring, debugging, and fine-grained fact manipulation.
This guide focuses on the provider chat path. For the session API, see the C ABI Reference.
ClipsInput JSON Reference
Section titled “ClipsInput JSON Reference”The user message JSON must conform to the ClipsInput schema. Unknown fields
are rejected (the engine uses strict deserialization).
{ "facts": [ {"template": "sensor", "values": {"name": "temp-1", "value": 150}} ], "templates": [ {"name": "alert", "slots": [{"name": "type", "type": "STRING"}, {"name": "severity", "type": "STRING"}]} ], "rules": [ {"name": "high-temp", "source": "(defrule high-temp (sensor (value ?v&:(> ?v 100))) => (assert (alert (type \"over-threshold\") (severity \"high\"))))"} ], "config": { "max_rules": 1000, "include_trace": true, "derived_only_new": true }, "focus": ["data-qc"], "globals": {"*threshold*": 100}}All fields are optional. The minimal valid input is {} (empty object).
| Field | Type | Description |
|---|---|---|
facts | array of {template, values} | Facts to assert before running inference |
templates | array of {name, slots} | Templates to create (if not in rule base) |
rules | array of {name, source} or {name, conditions, actions} | Rules to create programmatically |
config | object | Request-level overrides (see below) |
focus | array of strings | Module focus stack (controls which rules fire) |
globals | object | Global variable values to set |
command | string | Special command: "reset", "clear", "retract" |
modules | array of {name, doc, imports} | Modules to create |
policy_id | string | Cache key for session reuse |
Config fields:
| Field | Type | Default | Description |
|---|---|---|---|
max_rules | integer | -1 (unlimited) | Maximum rules to fire |
include_trace | boolean | false | Include rule firing trace in output |
derived_only_new | boolean | false | Only return newly derived facts |
output_templates | array of strings | all | Only return facts matching these templates |
Rule Loading
Section titled “Rule Loading”nxusKit’s CLIPS provider loads rules through the ClipsInput configuration. Rules can be loaded from:
- Text strings — CLIPS source passed directly via
rules_text - File paths —
.clpfiles loaded at runtime viarulesarray - Binary images — Pre-compiled
.binfiles viabinary_rules
Loading Order
Section titled “Loading Order”- Shared templates are loaded first (alphabetically by filename)
- Module rules are loaded next, in the order specified by the
focusconfiguration - User override rules are loaded last (taking precedence)
Rust Example
Section titled “Rust Example”use nxuskit::{AsyncProvider, ChatRequest, Message, NxuskitProvider, ProviderConfig};
let config = ProviderConfig { provider_type: "clips".into(), model: Some("/path/to/rules".into()), ..Default::default()};let provider = NxuskitProvider::new(config)?;
let request = ChatRequest::new("clips") .with_message(Message::user(r#"{"facts": [{"template": "input-data", "values": {"record-id": 1, "value": 150.0}}]}"#)) .with_provider_options(serde_json::json!({ "focus": ["data-qc"], "derived_only_new": true }));
let response = provider.chat(request).await?;println!("Alerts: {}", response.content);Go Example
Section titled “Go Example”import nxuskit "github.com/nxus-SYSTEMS/nxusKit/packages/nxuskit-go"
config := nxuskit.ProviderConfig{ ProviderType: "clips", Model: strPtr("/path/to/rules"),}provider, _ := nxuskit.NewProvider(config)
request := nxuskit.NewChatRequest("clips"). AddMessage(nxuskit.UserMessage(`{"facts": [{"template": "input-data", "values": {"record-id": 1, "value": 150.0}}]}`))request.ProviderOptions = map[string]interface{}{ "focus": []string{"data-qc"}, "derived_only_new": true,}
response, _ := provider.Chat(ctx, request)fmt.Println("Alerts:", response.Content)Python Example
Section titled “Python Example”from nxuskit-py import create_provider, Message
provider = create_provider("clips", model="/path/to/rules")
response = provider.chat( model="clips", messages=[Message.user('{"facts": [{"template": "input-data", "values": {"record-id": 1, "value": 150.0}}]}')], provider_options={ "focus": ["data-qc"], "derived_only_new": True, },)print("Alerts:", response.content)Writing Custom Rules
Section titled “Writing Custom Rules”1. Create a Rule File
Section titled “1. Create a Rule File”Place your .clp file in the appropriate module directory:
/path/to/my-rules/data-qc/my-custom-check.clp2. Reference Shared Templates
Section titled “2. Reference Shared Templates”Do NOT redefine templates. Use templates from the shared shared/*.clp files:
;;; my-custom-check.clp;;; Custom confidence check for strict environments
(defrule strict-confidence-check "Flag records with confidence below 0.8" (input-data (record-id ?rid) (confidence ?c&:(< ?c 0.8))) => (assert (alert (alert-type "low_confidence") (severity "warning") (message (str-cat "Record " ?rid " confidence " ?c " below threshold 0.8")) (recommendation "Review data source quality") (entity-id ?rid) (rule-name "strict-confidence-check") (module-name "data-qc"))))3. Use Configurable Thresholds
Section titled “3. Use Configurable Thresholds”Reference the threshold-config fact instead of hard-coding values:
(defrule configurable-bounds-check "Flag records outside configured bounds" (threshold-config (value-min ?vmin) (value-max ?vmax)) (input-data (record-id ?rid) (value ?v&:(or (< ?v ?vmin) (> ?v ?vmax)))) => (assert (alert (alert-type "value_out_of_bounds") (severity "high") (message (str-cat "Record " ?rid " value " ?v " outside [" ?vmin "-" ?vmax "]")) (recommendation "Check data or adjust threshold configuration") (entity-id ?rid) (rule-name "configurable-bounds-check") (module-name "data-qc"))))4. Naming Conventions
Section titled “4. Naming Conventions”- File names:
NNN-descriptive-name.clp(NNN = numeric prefix for load order) - Rule names:
kebab-case, descriptive of what is being checked - Alert types:
snake_case, machine-readable identifiers - Module names:
kebab-case, matching the directory name
Testing Custom Rules
Section titled “Testing Custom Rules”use nxuskit::{AsyncProvider, ChatRequest, Message, MockProvider};
// Unit test with MockProvider (no SDK binary required)#[tokio::test]async fn test_with_mock() { let provider = MockProvider::new(r#"{"alerts": [{"type": "low_confidence"}]}"#); let request = ChatRequest::new("clips") .with_message(Message::user("test input")); let response = provider.chat(request).await.unwrap(); assert!(response.content.contains("low_confidence"));}
// Integration test with real CLIPS engine (requires SDK binary)#[tokio::test]#[ignore = "requires libnxuskit runtime"]async fn test_with_clips_engine() { use nxuskit::{NxuskitProvider, ProviderConfig};
let config = ProviderConfig { provider_type: "clips".into(), model: Some("tests/rules".into()), ..Default::default() }; let provider = NxuskitProvider::new(config).unwrap(); let request = ChatRequest::new("clips") .with_message(Message::user(r#"{"facts": [{"template": "input-data", "values": {"record-id": 1, "value": 999.0}}]}"#)); let response = provider.chat(request).await.unwrap(); assert!(response.content.contains("out_of_bounds"));}func TestRulesWithMock(t *testing.T) { provider := nxuskit.NewMockProvider( nxuskit.WithResponse(`{"alerts": [{"type": "low_confidence"}]}`), ) req := nxuskit.NewChatRequest("clips"). AddMessage(nxuskit.UserMessage("test input")) resp, err := provider.Chat(context.Background(), req) require.NoError(t, err) assert.Contains(t, resp.Content, "low_confidence")}Python
Section titled “Python”import pytestfrom nxuskit-py import create_provider, Message
def test_rules_with_mock(): provider = create_provider("mock", responses=["low_confidence alert fired"]) response = provider.chat( model="clips", messages=[Message.user("test input")], ) assert "low_confidence" in response.contentDebugging Rules
Section titled “Debugging Rules”Enable Tracing
Section titled “Enable Tracing”Set the RUST_LOG environment variable when using the Rust SDK:
RUST_LOG=nxuskit=debug cargo test -- --nocaptureThis shows:
- Which rule files are loaded
- Module loading order and file counts
- Warnings for syntax errors
Inspect CLIPS Facts
Section titled “Inspect CLIPS Facts”Access the CLIPS session API directly (Rust SDK):
use nxuskit::ClipsSession;
let session = ClipsSession::create()?;session.load_string("(deftemplate input-data (slot record-id (type INTEGER)) (slot value (type FLOAT)))")?;session.load_string("(deftemplate alert (slot alert-type (type STRING)) (slot severity (type STRING)))")?;session.load_string(r#"(defrule check (input-data (record-id ?rid) (value ?v&:(> ?v 100.0))) => (assert (alert (alert-type "over-threshold") (severity "high"))))"#)?;session.reset()?;session.assert_string(r#"(input-data (record-id 1) (value 150.0))"#)?;session.run(-1)?;
// List facts by templatelet facts_json = session.facts_by_template("alert")?;println!("Alert facts: {}", facts_json);
// Drop destroys the session automaticallyStep Limit Debugging
Section titled “Step Limit Debugging”If inference hits the step limit, check for rule cycles. Common causes:
- Rules that assert facts matching their own LHS (infinite loop)
- Missing
notpatterns allowing rules to fire repeatedly - Very large fact sets with cross-product patterns
Increase the step limit if needed:
session.run(500000)?; // Pass -1 to run until agenda exhaustedBest Practices
Section titled “Best Practices”- Keep rules simple — One check per rule. Complex logic should be split across multiple rules.
- Use configurable thresholds — Reference threshold facts instead of hard-coding values.
- Document every rule — Use the CLIPS documentation string for a brief description.
- Test in isolation — Load only shared templates and the single rule file under test.
- Use meaningful names — Rule names should describe what is checked, not how.
- Set appropriate salience — Use
(declare (salience N))to control firing order when needed.