Chapter 7: The Noul Primitive: Yes/No with Real Probabilities

Learning Objectives

Pre-Quiz: What a Noul Is

What does the number returned by a Noul actually represent?

The probability that the statement in instructions is true
A confidence score describing how certain Jev is in a separate Choice answer
A severity rating on a 0-10 scale rescaled to 0-1
The percentage of similar past cases that were true

You write a Noul instruction as "The interface is stable" and plan to treat any value at or above 0.7 as "this interface is flapping." What is wrong with this design?

Nothing — 0.7 is a reasonable threshold for flapping detection
The instruction's direction is inverted, so a high probability means "not flapping," making the threshold backwards
The instruction should have used criteria instead of instructions
Flapping should be a Score primitive, not a Noul

A Noul returns 0.5 for "Does this Splunk event indicate a routing loop?" What is the recommended response?

Treat it as a weak "yes" and log it at low severity
Average it with other signals so it contributes half-weight to a composite score
Route it to a human, and consider whether the question or the state needs fixing
Treat it as a "no" since 0.5 is below the classic action threshold of 0.7

What a Noul Is

Key Points

A Probability That a Statement Is True, From 0 to 1

A Noul is the TypeSafe primitive for yes/no questions. You hand Jev a statement or a question about your state, and it returns a single number between 0 and 1: the probability that the answer is yes. That is the whole answer — there is no label, no rubric, no prose explanation, just a truth probability.

A Noul question has three parts: type set to "noul", a required instructions field holding the yes/no question, and an optional criteria field clarifying what true and false mean. In Python, the SDK class supplies type for you:

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient()

response = client.system_one(
    state=syslog_line,
    questions={
        "is_flap": Noul(
            instructions="The message reports an interface repeatedly changing state",
        ),
    },
)

print(response.answers["is_flap"].noul)  # e.g. 0.94

The .noul attribute is the probability of yes. Phrase instructions so that a high probability unambiguously means "yes." If you write "The interface is stable" and then threshold at 0.7 to mean "flapping," you have built a bug into your control flow that no amount of testing the model will find.

Reach for a Noul whenever the judgment is genuinely binary: does this configuration contain a telnet line, does this Splunk alert describe a customer-facing outage, does this change ticket name a rollback owner. Reach for Choice instead when the answer is one of several named options, and for Score when the answer is a position on a spectrum. All three primitives can be evaluated in parallel against the same state in one request.

0.5 Means Equal Uncertainty, Not a Medium Value

This is the single most important thing to internalize about the Noul, and it is where engineers coming from monitoring thresholds get burned.

A Score answer returns an expected score plus a separate confidence value, and a Choice answer returns a selected label plus confidence and a probabilities distribution across labels. A Noul returns neither — only the noul field, with no separate confidence calculation. The probability itself encodes both direction and certainty, which is why a value of 0.5 does not mean "medium skill" or "medium anything."

Think of a QoS DSCP marking. AF31 is not "half of EF" — it is a different class with different meaning. A Noul of 0.5 is not "half true" or "medium severity." It means Jev assigns roughly equal probability to yes and no — the evidence in your state genuinely does not settle the question. The right response to 0.5 is almost never "treat it as a moderate hit." It is "a human needs to look at this, or my question was badly written, or my state was missing the field that would decide it."

Noul valueWhat it meansTypical NOC action
0.05Strong no. The evidence clearly contradicts the statement.Treat as false. Suppress the check, close the finding, move on without a note.
0.30Probably no, but not clean. Some signal points the other way.Treat as false for automation, but log the value. Sits just below the 0.35 review floor.
0.50Equal probability for yes and no — genuine ambiguity, not a mid-level score.Do not act automatically. Route to a human, and inspect whether the question or the state is the problem.
0.70Probably yes. Enough to act on for low-risk and notify-only actions.Common action threshold: open or enrich a ServiceNow ticket, raise severity, page the on-call for a notify-class event.
0.95Strong yes. The evidence clearly supports the statement.Treat as true. Safe to drive automated remediation if the action class also permits it.

Those cut points are not arbitrary. The TypeSafe guardrails cookbook builds its screening pipeline on a review threshold around 0.35 that sends a case to human review, and an action threshold around 0.70 that triggers the configured automatic response. The consistency cookbook describes the same idea with its lower edge at 0.30. The gap between 0.30 and 0.35 is not meaningful — both are saying "below roughly a third, stop looking." This book uses 0.35 as the review floor throughout, so that the number in the figures matches the number in the code, and both cut points get scaled to network risk later in this chapter.

Figure 7.1: The Noul truth probability scale, with the neutral band and review/action thresholds

flowchart LR A["Below 0.35: Strong No"] --> B["0.35 to 0.69: Neutral Band - Route to Human Review"] B --> C["0.70 to 0.94: Probably Yes - Act on Low Risk"] C --> D["0.95 to 1.0: Strong Yes - Safe for Automated Action"]

Visual animation — coming soon

Analogy: A Route-Map Match With a Confidence Attached

Network engineers already think in binaries with a fuzzy edge, so the Noul has a natural analogy. A BGP prefix either matches a route-map clause or it does not — 10.1.0.0/16 either falls inside the prefix-list or it falls outside. There is no 0.7 match; the routing table is deterministic, and that determinism is exactly what makes it safe to build policy on.

Now imagine the same match, but the thing being matched is not a prefix. It is a sentence in a change ticket, or a paragraph of vendor release notes, or a free-text incident description typed by a field tech at 2 a.m. The question is still perfectly binary — "does this describe a hardware fault?" — but the input is not. A Noul gives you the route-map answer you wanted, with an honest label on how clean the match was.

That extra number changes what you are allowed to build. A regex-based syslog rule either fires or it does not, and when a message is worded slightly differently than your pattern expected, it silently does not fire. A Noul distinguishes three cases that a regex collapses into two: a clear match (0.95), a clear miss (0.05), and a message your rule genuinely cannot classify (0.5). That third case is where NOC automation historically fails quietly, and where a Noul lets you fail loudly and hand the case to a person instead.

The consistency of those numbers is what makes them usable as control-flow inputs. TypeSafe's consistency cookbook measures Noul stability by repeating identical calls and computing the standard deviation of each question's probability, reporting a mean standard deviation of 0.0102 against higher variation from general reasoning models. A threshold at 0.70 is only meaningful if repeated evaluations of the same syslog line cluster tightly; if the answer wandered between 0.4 and 0.9 on identical input, no threshold would hold.

Post-Quiz: What a Noul Is

What does the number returned by a Noul actually represent?

The probability that the statement in instructions is true
A confidence score describing how certain Jev is in a separate Choice answer
A severity rating on a 0-10 scale rescaled to 0-1
The percentage of similar past cases that were true

You write a Noul instruction as "The interface is stable" and plan to treat any value at or above 0.7 as "this interface is flapping." What is wrong with this design?

Nothing — 0.7 is a reasonable threshold for flapping detection
The instruction's direction is inverted, so a high probability means "not flapping," making the threshold backwards
The instruction should have used criteria instead of instructions
Flapping should be a Score primitive, not a Noul

A Noul returns 0.5 for "Does this Splunk event indicate a routing loop?" What is the recommended response?

Treat it as a weak "yes" and log it at low severity
Average it with other signals so it contributes half-weight to a composite score
Route it to a human, and consider whether the question or the state needs fixing
Treat it as a "no" since 0.5 is below the classic action threshold of 0.7
Pre-Quiz: Clarifying Yes and No

When should you add a criteria field to a Noul question?

Always, for every Noul you write, regardless of the question
Only when using the Python SDK, never with raw JSON requests
When the yes/no boundary is subtle and needs an explicit operational definition
Only when combining more than three Nouls in one request

Why does the chapter say "The config does not fail to include a local fallback" is a badly worded Noul instruction?

It is too long for the model to process reliably
It contains a double negative that makes it unclear which direction a high probability points
It uses technical jargon that Jev cannot understand
It should have been phrased as a Choice question instead

The chapter recommends naming "the two cases your teammates would argue about" when writing a Noul. What is this practice meant to catch?

Cases where Jev's inference speed is too slow
Ambiguous edge cases that your wording implicitly assumes are settled but doesn't actually decide
Situations requiring a weighted sum instead of a Noul
Requests that should be split across multiple API calls for cost reasons

Clarifying Yes and No

Key Points

Optional Criteria: Describing What Counts as Yes and No

The instructions field carries the question. The optional criteria field carries the definitions — descriptions clarifying what true and false outcomes mean, recommended "when yes/no boundaries are subtle."

Subtle boundaries are the normal case in network operations. "Does this config disable local authentication fallback?" is subtle, because a config can disable it for SSH and keep it for console. Criteria are where you write down the operational definition your team already uses informally:

Noul(
    instructions=(
        "The SSH login authentication path has no local fallback if the "
        "remote AAA servers are unreachable"
    ),
    criteria={
        "true": (
            "The aaa authentication login ssh line lists only remote server "
            "groups such as tacacs+ or radius, with no local keyword, so an "
            "unreachable AAA server leaves no way to log in over SSH."
        ),
        "false": (
            "The aaa authentication login ssh line ends with the local "
            "keyword, or no aaa authentication login ssh line is present at "
            "all so the switch still uses its local user database."
        ),
    },
)

That true/false shape is the documented one. Compare it with the other two primitives: Choice takes criteria as a mapping of option name to description, and Score takes it as an ordered list of level descriptions. Note also that a Noul with instructions alone is fully valid — every cookbook example builds its question dictionary from instructions only. Reach for criteria when the boundary needs spelling out, not by default.

The payoff is that the definition lives in version control instead of in a senior engineer's head. When the NOC argues about whether a maintenance-window ticket counts as "customer-affecting," that argument is resolved once, in a string, and every subsequent evaluation applies the same rule.

Stating Boundary Cases Explicitly, Because Jev Interprets Literally

Jev answers the question you asked. Literal interpretation is a feature — it is what makes the probabilities stable — but it means the model will not silently import the assumptions you did not write down.

The network equivalents are everywhere:

A useful discipline: for every Noul you write, name the two cases your teammates would argue about, and make sure the instruction or criteria decides both. If you cannot decide them, that is a sign the judgment is really a Choice with three options, one of which is "unclear."

Avoiding Double Negatives and Indirection

Ambiguous questions reduce the interpretability of the probability. In practice the ambiguity almost always comes from one of four sources: negation, vagueness, compounding, or a mismatch between the question's direction and the threshold in your code.

Badly worded instructionWhy it failsWell worded instruction
"The config does not fail to include a local fallback."Double negative. A high probability now means fallback is present — the opposite of the risk you are screening for."The SSH login authentication path has no local fallback if the remote AAA servers are unreachable."
"Is this config OK?"Vague. "OK" has no definition, so the probability reflects the model's guess at your standard, not your standard."The running configuration contains at least one vty line with transport input telnet."
"The change is high risk and lacks a rollback plan."Compound. Two independent facts share one number, and a 0.5 could mean either half is true.Two separate Nouls: one for blast radius, one for rollback plan presence.
"Should we escalate this alert?"Asks for a decision, not a fact. The escalation policy belongs in your code, not in the model."The event describes a forwarding loop between two or more routers."
"The device is not unreachable."Negation plus indirection. Every reader has to translate it twice."The device responded to the most recent polling attempt."
"This looks like a routing problem."Hedged and unbounded. "Looks like" and "problem" are undefined, so repeated runs drift."The event indicates packets are traversing the same set of routers repeatedly before their TTL expires."

The pattern behind every fix is the same: state a checkable fact, in the positive, one fact per question, and let your code own the policy. The model supplies the observation; the if statement supplies the decision.

Visual animation — coming soon

Post-Quiz: Clarifying Yes and No

When should you add a criteria field to a Noul question?

Always, for every Noul you write, regardless of the question
Only when using the Python SDK, never with raw JSON requests
When the yes/no boundary is subtle and needs an explicit operational definition
Only when combining more than three Nouls in one request

Why does the chapter say "The config does not fail to include a local fallback" is a badly worded Noul instruction?

It is too long for the model to process reliably
It contains a double negative that makes it unclear which direction a high probability points
It uses technical jargon that Jev cannot understand
It should have been phrased as a Choice question instead

The chapter recommends naming "the two cases your teammates would argue about" when writing a Noul. What is this practice meant to catch?

Cases where Jev's inference speed is too slow
Ambiguous edge cases that your wording implicitly assumes are settled but doesn't actually decide
Situations requiring a weighted sum instead of a Noul
Requests that should be split across multiple API calls for cost reasons
Pre-Quiz: Nouls as Building-Block Signals

Why does the guardrails cookbook screen a message with four Noul questions plus a Score in a single request, rather than sending several separate requests?

Because Jev requires at least five questions per call to function
Because the token cost is dominated by the state, so evaluating more questions against the same state in one request is far cheaper than one request per question
Because Noul and Score answers cannot be returned in separate requests
Because only one primitive type is allowed per TypeSafeClient instance

In the Aruba lockout-risk function, why does the chapter use boolean composition (and/not) rather than a weighted sum?

Because weighted sums cannot be implemented in Python
Because the lockout risk requires all three specific conditions to hold simultaneously, and a weighted sum could let two strong signals outvote a missing third
Because boolean composition is always more accurate than weighted sums
Because the Aruba documentation requires the use of and specifically

Why does the chapter recommend a much higher action threshold (0.95 plus corroboration) for destructive actions like shutting an interface, compared to 0.50 for adding a ticket note?

Because destructive actions require a second independent signal in addition to a very high probability, since the cost of being wrong is far higher
Because the model is measurably less accurate on destructive-action questions
Because 0.95 is the maximum value a Noul can return for high-risk questions
Because syslog severity levels cap out at 0.95 for critical events

Nouls as Building-Block Signals

Key Points

Asking Many Nouls in One Request

A single Noul is rarely the interesting object. The interesting object is a battery of them evaluated against the same state in one call, each one a signal that your code weighs. The TypeSafe guardrails cookbook is built exactly this way: it screens a message with one request carrying four Noul questions — jailbreak attempt, harmful request, medical advice, self-harm signal — plus a Score rating how much harm complying would do.

Swap hazards for misconfigurations and you have a config compliance scanner:

from typesafe_sdk import Noul, TypeSafeClient

HARDENING_CHECKS = {
    "telnet_enabled": "The configuration permits telnet for management access",
    "weak_snmp": "The configuration contains a default SNMP community string such as public or private",
    "no_remote_syslog": "The configuration sends no logs to a remote syslog server",
    "no_ntp": "The configuration defines no NTP server",
    "mgmt_in_data_vrf": "SSH is bound to the default VRF rather than a dedicated management VRF",
}

client = TypeSafeClient()

response = client.system_one(
    state=running_config,
    questions={
        key: Noul(instructions=question)
        for key, question in HARDENING_CHECKS.items()
    },
)

findings = {key: answer.noul for key, answer in response.nouls.items()}

Every check there comes from published hardening guidance. Telnet transmits credentials in cleartext and should be replaced with SSHv2, and default SNMP communities allow unauthorized reconnaissance and configuration retrieval. On AOS-CX, device-local logs are lost if the device is compromised or reboots, clock skew from a missing NTP server breaks authentication timestamps and log correlation, and binding SSH to vrf default instead of vrf mgmt leaves the management plane reachable from data VLANs.

The response.nouls collection holds the yes/no answers keyed by question name, alongside response.choices and response.scores for the other primitives and response.answers for all of them together. Because one request carries the whole battery, the token cost is dominated by the state — the config you sent once — not by the number of questions you asked about it.

Combining Nouls in Code: Boolean Logic and Weighted Sums

Once you have a dictionary of probabilities, you combine them. There are two idioms, and they are good at different things.

Boolean composition thresholds each Noul into a hard true/false and then applies ordinary logic. Use this when the rule you are encoding is genuinely logical — when a specific combination of facts, not an accumulation of concern, is what matters.

def is_lockout_risk(nouls: dict[str, float]) -> bool:
    """Remote AAA with no local fallback and no fail-through is a lockout risk."""
    no_local_fallback = nouls["no_local_fallback"] >= 0.70
    remote_aaa_configured = nouls["remote_aaa_configured"] >= 0.70
    fail_through_set = nouls["authorization_fail_through"] >= 0.70
    return remote_aaa_configured and no_local_fallback and not fail_through_set

That function encodes a real Aruba failure mode: AOS-CX guidance is that when command authorization is configured without authorization fail-through, a remote AAA server failure leaves the device unusable, so aaa authorization allow-fail-through must be set before configuring authentication fail-through to prevent lockout. Notice that the risk requires all three conditions — the and is doing real work. A weighted sum would let two strong signals outvote the missing third and produce a finding that is not actually a lockout risk.

Figure 7.2: Combining several Nouls with boolean logic into a lockout-risk decision

flowchart TD A{"Remote AAA Configured (>= 0.70)?"} -- No --> E["Not a Lockout Risk"] A -- Yes --> B{"No Local Fallback (>= 0.70)?"} B -- No --> E B -- Yes --> C{"Authorization Fail-Through Set (>= 0.70)?"} C -- Yes --> E C -- No --> D["Lockout Risk: Open ServiceNow Change"]

Weighted sums keep the probabilities as numbers and blend them into one composite score. Use this when you are accumulating concern rather than testing a rule — grading a device's overall hardening posture, or ranking a queue of change tickets by how much review they deserve.

CHECK_WEIGHTS = {
    "telnet_enabled": 0.30,
    "weak_snmp": 0.25,
    "no_local_fallback": 0.20,
    "no_remote_syslog": 0.15,
    "no_ntp": 0.10,
}

def hardening_risk(nouls: dict[str, float]) -> float:
    """Weighted blend of independent hardening signals, 0.0 to 1.0."""
    total = sum(CHECK_WEIGHTS.values())
    return sum(nouls[key] * weight for key, weight in CHECK_WEIGHTS.items()) / total

Weights are yours to set, and they should reflect operational consequence rather than how often a check fires. Telnet and default SNMP communities carry the heaviest weight above because the management plane, not the data plane, is what device compromises typically exploit.

Two warnings. First, keep the arithmetic in Python — Jev is a fast judgment model, not a calculator, and the composite score is your code's responsibility: the model supplies observations, your code supplies control flow. Second, a weighted sum hides which signal fired. Always carry the underlying probabilities forward into your ticket so the engineer who opens it sees telnet_enabled: 0.97 and not only risk: 0.61.

Thresholds Scaled to Risk: Read-Only Versus Destructive Actions

A threshold is a policy decision, not a model property. The same 0.78 probability should trigger a ServiceNow comment and should absolutely not trigger a write memory.

The base pattern comes from the guardrails cookbook: a review threshold near 0.35 that routes a case to human review, and an action threshold near 0.70 that triggers the configured automatic response, with a severity score able to override and escalate a review into a block. Don't use a single cut point at 0.5 — probabilities 0.49 and 0.51 would cause opposite actions even though both express substantial uncertainty. Define a neutral band instead, roughly 0.35 to 0.70, that routes uncertain cases to human review.

Scale that idea to the blast radius of the action your code is about to take:

Action classExample in the NOC triage serviceAct-on-yes thresholdHuman-review bandRationale
Read-only / enrichmentAdd a note to a ServiceNow ticket, tag a Splunk event, set a dashboard label0.50noneBeing wrong costs an extra sentence in a ticket. Bias toward coverage.
Routing / assignmentAssign the ticket to the routing team versus the wireless team0.600.40–0.60 falls back to the general queueA misroute costs minutes of handoff, and the neutral band has a safe default.
Notify / escalateRaise severity, page the on-call, open a Salesforce case for a customer0.700.35–0.70 goes to a review queueMatches the cookbook's action and review thresholds. False pages erode trust fast.
Non-service-affecting config changePush the missing local keyword onto an AAA line, add a syslog destination0.85everything from 0.35 to 0.85 goes to a humanA write to a production device deserves near-certainty, plus a peer review step.
Destructive / service-affectingShut an interface, reload a device, withdraw a BGP prefix, execute a rollback0.95 and a second independent signaleverything below 0.95Never let one probability take down a link. Require corroboration and an approval.

Three rules make this table safe to operate. Require corroboration for destructive actions — change-management practice for high-risk work requires a minimum of two engineers, an implementer and an observer who validates each step, precisely because a single judgment is not enough. Your automation should mirror that: a second Noul, a telemetry check, or a human click. Mind the direction of the question — if your Noul asks "the change is safe to apply automatically," you need a high probability to proceed; if it asks "the change carries customer-facing risk," you need a low probability to proceed. Confusing the two is the single most common way a threshold table gets inverted in production. Log the probability, not just the decision — six weeks later, "routing_loop came back 0.91 against a 0.70 notify threshold" is auditable, while "the AI thought it was a loop" is not.

Figure 7.3: The risk-scaled threshold ladder, from read-only actions to destructive actions

flowchart LR A["Read-Only or Enrichment: Threshold 0.50"] --> B["Routing or Assignment: Threshold 0.60"] B --> C["Notify or Escalate: Threshold 0.70"] C --> D["Non-Service-Affecting Config Change: Threshold 0.85"] D --> E["Destructive or Service-Affecting: Threshold 0.95 Plus Corroboration"]

Visual animation — coming soon

Post-Quiz: Nouls as Building-Block Signals

Why does the guardrails cookbook screen a message with four Noul questions plus a Score in a single request, rather than sending several separate requests?

Because Jev requires at least five questions per call to function
Because the token cost is dominated by the state, so evaluating more questions against the same state in one request is far cheaper than one request per question
Because Noul and Score answers cannot be returned in separate requests
Because only one primitive type is allowed per TypeSafeClient instance

In the Aruba lockout-risk function, why does the chapter use boolean composition (and/not) rather than a weighted sum?

Because weighted sums cannot be implemented in Python
Because the lockout risk requires all three specific conditions to hold simultaneously, and a weighted sum could let two strong signals outvote a missing third
Because boolean composition is always more accurate than weighted sums
Because the Aruba documentation requires the use of and specifically

Why does the chapter recommend a much higher action threshold (0.95 plus corroboration) for destructive actions like shutting an interface, compared to 0.50 for adding a ticket note?

Because destructive actions require a second independent signal in addition to a very high probability, since the cost of being wrong is far higher
Because the model is measurably less accurate on destructive-action questions
Because 0.95 is the maximum value a Noul can return for high-risk questions
Because syslog severity levels cap out at 0.95 for critical events
Pre-Quiz: Worked Examples

In the Aruba example, why is authorization_fail_through checked with a LOW-probability comparison (below the review floor) rather than a HIGH-probability comparison like the other two checks?

Because the underlying question was written so a HIGH probability confirms fail-through IS enabled, so proving it is ABSENT requires a LOW probability
Because fail-through is inherently harder for Jev to detect than the other two settings
Because inverted checks are the only case where TypeSafe requires a criteria field
Because the SDK returns fail-through checks as a Choice object instead of a Noul

What role does the maintenance_expected Noul play in the Splunk routing-loop detection code?

It replaces the need for the other three Noul checks entirely
It acts as a suppressor: the page only fires when this probability is low, confirming the activity is not expected maintenance
It is averaged together with the other three signals into a single composite score
It sets the actual severity level passed to page_oncall

In the change-request example, rollback_present returns 0.93 but rollback_executable returns 0.06. What does this pairing demonstrate?

That the Noul battery is unreliable and produced a contradictory result
That a rollback section can exist in the ticket text while being operationally worthless — a distinction a keyword search for "rollback" cannot make
That rollback_present should have been weighted more heavily than rollback_executable
That the two checks should be merged into a single Noul to avoid confusion

Worked Examples

Key Points

Example 1: Does This Aruba Config Disable Local Authentication Fallback?

A NOC compliance job pulls running configurations nightly from the Aruba AOS-CX access layer. One switch returns this fragment:

aaa authentication login ssh group tacacs+
aaa authentication login console group tacacs+ local
aaa authorization commands ssh group tacacs+
ssh server vrf mgmt

Read it the way an engineer reads it. The console path ends in local, so someone at the switch with a serial cable can still log in when TACACS+ is down. The SSH path does not. Both problems are present, and neither is caught by a grep for "tacacs" — the string is on every line.

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient()

aruba_config = """aaa authentication login ssh group tacacs+
aaa authentication login console group tacacs+ local
aaa authorization commands ssh group tacacs+
ssh server vrf mgmt"""

response = client.system_one(
    state=aruba_config,
    questions={
        "no_local_fallback": Noul(
            instructions=(
                "The SSH login authentication path has no local fallback, so an "
                "unreachable TACACS+ or RADIUS server would block SSH logins"
            ),
            criteria={
                "true": (
                    "The aaa authentication login ssh line lists only remote "
                    "server groups and does not end with the local keyword."
                ),
                "false": (
                    "The aaa authentication login ssh line ends with local, or "
                    "there is no aaa authentication login ssh line at all so the "
                    "switch still uses its local user database."
                ),
            },
        ),
        "remote_aaa_configured": Noul(
            instructions="The configuration authenticates logins against a remote TACACS+ or RADIUS server group",
        ),
        "authorization_fail_through": Noul(
            instructions="The configuration enables authorization fail-through so AAA server failure does not lock operators out",
        ),
    },
)

nouls = {key: answer.noul for key, answer in response.nouls.items()}
# {'no_local_fallback': 0.96, 'remote_aaa_configured': 0.98,
#  'authorization_fail_through': 0.04}

The probabilities shown in the comment are illustrative of a clean case; your own values will vary with the config you send. What matters is the shape of the decision built on top of them:

LOCKOUT_ACTION_THRESHOLD = 0.85   # config-change class
REVIEW_FLOOR = 0.35               # standardized review floor throughout this book

if (nouls["remote_aaa_configured"] >= 0.70
        and nouls["no_local_fallback"] >= LOCKOUT_ACTION_THRESHOLD
        and nouls["authorization_fail_through"] < REVIEW_FLOOR):
    open_servicenow_change(
        short_description="AOS-CX lockout risk: SSH AAA has no local fallback",
        risk="high",
        evidence=nouls,
    )

Three choices there are deliberate. The no_local_fallback check uses the 0.85 config-change threshold because the remediation writes to a production switch. The authorization_fail_through check is inverted — we want a low probability to confirm the setting is absent — and it is compared against the review floor rather than the action threshold, because a mid-range value there means "I could not tell," which should not silently pass. And the whole nouls dictionary rides into the ticket, so the engineer who picks it up sees each signal rather than a verdict.

Check the false case too. A switch with aaa authentication login ssh group tacacs+ local should produce a low no_local_fallback probability, and so should a switch with no AAA configuration at all — because the criteria explicitly say so. That second case is the boundary the criteria exist to settle.

Example 2: Does This Splunk Event Indicate a Routing Loop?

Splunk fires a webhook to the triage service when a saved search matches. The payload carries result with the first matching event row, plus sid, results_link, search_name, owner, and app.

{
  "search_name": "Core - TTL exceeded spike with route churn",
  "sid": "scheduler_admin_network_alerts_1758100200_318",
  "app": "network",
  "owner": "noc_automation",
  "results_link": "http://splunk.example.com:8000/app/network/@go?sid=...",
  "result": {
    "_time": "2026-09-17T02:14:33Z",
    "host": "core-rtr-01",
    "ttl_exceeded_per_min": 4820,
    "affected_prefix": "10.42.7.0/24",
    "recent_syslog": "*Sep 17 02:14:09.112: %OSPF-5-ADJCHG: Process 1, Nbr 10.0.0.9 on TenGigabitEthernet0/0/2 from FULL to DOWN, Neighbor Down: Dead timer expired | ...",
    "traceroute_excerpt": "6  10.0.0.9  1.9 ms\n 7  10.0.0.5  2.1 ms\n 8  10.0.0.9  2.4 ms\n 9  10.0.0.5  2.6 ms\n10  10.0.0.9  2.9 ms"
  }
}

Two independent facts point at a loop. The traceroute alternates between the same two next hops, and the %OSPF-5-ADJCHG messages show the same neighbor cycling between FULL and DOWN within ten seconds. Either alone is ambiguous: a TTL-exceeded spike can come from a traceroute-heavy monitoring host, and a single adjacency reset is routine. Together they are a loop — exactly the structure a Noul battery plus boolean composition handles well.

LOOP_CHECKS = {
    "ttl_expiry_spike": (
        "The event reports an elevated rate of packets whose TTL expired in transit"
    ),
    "repeating_hops": (
        "The traceroute output shows the same router addresses appearing more than "
        "once in an alternating pattern"
    ),
    "adjacency_churn": (
        "The syslog excerpt shows a routing adjacency with the same neighbor going "
        "down and coming back up more than once"
    ),
    "maintenance_expected": (
        "The event text states that this activity is expected during a planned "
        "maintenance window"
    ),
}

response = client.system_one(
    state=webhook_payload["result"],
    questions={key: Noul(instructions=q) for key, q in LOOP_CHECKS.items()},
)
n = {key: answer.noul for key, answer in response.nouls.items()}
# {'ttl_expiry_spike': 0.97, 'repeating_hops': 0.95,
#  'adjacency_churn': 0.93, 'maintenance_expected': 0.02}

NOTIFY = 0.70          # notify/escalate class
REVIEW_FLOOR = 0.35    # below this, stop looking

forwarding_evidence = sum(
    1 for key in ("ttl_expiry_spike", "repeating_hops", "adjacency_churn")
    if n[key] >= NOTIFY
)

if forwarding_evidence >= 2 and n["maintenance_expected"] < REVIEW_FLOOR:
    page_oncall(team="routing", severity=2, evidence=n,
                splunk_link=webhook_payload["results_link"])
elif forwarding_evidence >= 1:
    queue_for_human_review(evidence=n,
                           splunk_link=webhook_payload["results_link"])

The forwarding_evidence >= 2 test is the corroboration rule from the threshold table, expressed as a count of independent signals rather than an average — averaging would let a single 0.99 drag a composite over the line. The maintenance_expected Noul is a suppressor and runs in the opposite direction: we proceed only when we are confident this is not expected work. Because page_oncall receives the whole n dictionary and the results_link, the engineer who wakes up gets both the probabilities and one click back to the raw Splunk search.

Example 3: Is This Change Request Missing a Rollback Plan?

The last example runs against text a human wrote, which is where Nouls handle inputs that no parser could. A ServiceNow change request arrives for CAB review:

CHG0041892 — Migrate core uplink from Gi0/0/1 to Te0/0/3 (dc1-core-01)
Requested by: j.okafor    Window: 2026-09-20 02:00-04:00 UTC    Risk: Medium

Description:
Move the northbound uplink from the 1G copper interface to the new 10G optic on
Te0/0/3. New optic is installed and shows light. BGP session to the upstream
carrier will be rebuilt on the new interface with the same peer IP and ASN.

Implementation steps:
1. Shut Gi0/0/1
2. Configure Te0/0/3 with the existing uplink IP and description
3. Move the BGP neighbor statement to the new interface
4. Verify BGP session comes up and full table is received
5. Save config

Backout: If there are problems we will put it back the way it was.

Validation: Confirm the carrier session is established.

This ticket has a backout line, so a keyword search for "backout" or "rollback" marks it complete. It is not. Industry guidance is direct: generic "revert the config" statements are insufficient, and a real rollback plan names a point-in-time target, a step-by-step command sequence, an owner with console access, validation criteria, an estimated duration, and the backup location. The ticket also never mentions out-of-band access, which matters here — step 1 shuts the interface the engineer may be reaching the router through.

CHANGE_CHECKS = {
    "rollback_present": "The request contains a backout or rollback section of any kind",
    "rollback_executable": (
        "The rollback description lists the specific commands or configuration "
        "restore steps an engineer would run, rather than a general statement of "
        "intent to revert"
    ),
    "restore_point_named": (
        "The request identifies the specific saved configuration or point in time "
        "that a rollback would restore"
    ),
    "rollback_owner_named": "The request names the person responsible for executing a rollback",
    "oob_access_stated": (
        "The request states that console or out-of-band management access is "
        "available during the change window"
    ),
    "validation_measurable": (
        "The post-change validation steps state measurable pass criteria such as "
        "expected route counts, convergence times, or interface error counters"
    ),
}

response = client.system_one(
    state=change_request_text,
    questions={key: Noul(instructions=q) for key, q in CHANGE_CHECKS.items()},
)
c = {key: answer.noul for key, answer in response.nouls.items()}
# {'rollback_present': 0.93, 'rollback_executable': 0.06,
#  'restore_point_named': 0.03, 'rollback_owner_named': 0.05,
#  'oob_access_stated': 0.04, 'validation_measurable': 0.11}

rollback_present at 0.93 and rollback_executable at 0.06 is the exact pattern that keyword matching cannot see: the section exists, and it is worthless. Now grade the ticket with a weighted sum, because here we genuinely are accumulating concern rather than testing a logical rule:

READINESS_WEIGHTS = {
    "rollback_executable": 0.30,
    "restore_point_named": 0.20,
    "rollback_owner_named": 0.15,
    "oob_access_stated": 0.20,
    "validation_measurable": 0.15,
}

def readiness(c: dict[str, float]) -> float:
    total = sum(READINESS_WEIGHTS.values())
    return sum(c[k] * w for k, w in READINESS_WEIGHTS.items()) / total

score = readiness(c)   # ~0.06

if score < 0.35:
    reject_to_requester(
        reason="Rollback plan is not executable as written",
        gaps=[k for k, v in c.items() if v < 0.35 and k != "rollback_present"],
        evidence=c,
    )
elif score < 0.70:
    route_to_cab(evidence=c)
else:
    auto_approve_standard_change(evidence=c)

The three bands are the review and action thresholds again, applied to a composite. Note what the automation does not do: it never approves a change on its own judgment alone — the top branch applies only to changes already classified as standard, and everything ambiguous goes to the Change Advisory Board. The gaps list turns the rejection into actionable feedback: add a restore point, an owner, and out-of-band confirmation.

Figure 7.4: Worked example — the change-request rollback check, from submission to CAB routing

sequenceDiagram participant SN as ServiceNow Change Request participant Noul as Noul Battery participant Score as Readiness Scorer participant CAB as Change Advisory Board participant Req as Requester SN->>Noul: Send change request text as state Noul-->>Score: Return six check probabilities Score->>Score: Compute weighted readiness score alt Score below 0.35 Score->>Req: Reject with list of failing checks else Score between 0.35 and 0.70 Score->>CAB: Route for manual review else Score 0.70 or above Score->>SN: Auto-approve as standard change end

The payoff is measurable: performing both pre-change and post-change validation is associated with change-related incidents dropping by 70–80%. The mechanisms that make a restore point meaningful also differ by vendor — Junos offers atomic rollback 1 and commit confirmed, Arista offers abandonable configuration sessions, AOS-CX offers checkpoints, and IOS requires configure replace or manual reversal. A rollback plan that does not name which one it uses is a plan nobody can execute at 3 a.m.

Visual animation — coming soon

Chapter Summary

The Noul is the simplest TypeSafe primitive and the one most likely to be misread. It answers a yes/no question with a single number between 0 and 1 — the probability that the answer is yes — and that number carries both the direction of the judgment and the certainty behind it, with no separate confidence field to consult. A 0.5 is not a medium finding; it is a declaration that the state does not settle the question. Because the probability is what you threshold on, the phrasing rule matters more than anything else in this chapter: write instructions as a single positive checkable fact so that high always means yes, and use the optional criteria to write down the boundary cases your team would otherwise argue about.

The power of the primitive shows up in composition, not in isolation. A battery of small Nouls evaluated against one state in one request gives you a set of independent signals, and your code decides what to do with them. Boolean composition fits when a specific combination of facts defines the finding; a weighted sum fits when you are accumulating concern. Either way, the arithmetic belongs in Python, the individual probabilities travel with the decision into the ticket, and the model never owns the control flow.

Thresholds belong to the action rather than to the model. The published pattern of a review band near 0.35 and an action threshold near 0.70 is a starting point that you stretch or shrink according to blast radius: 0.50 for adding a note to a ServiceNow ticket, 0.70 for paging the on-call, 0.85 for writing a line to a production switch, and 0.95 plus an independent corroborating signal before anything that shuts an interface or reloads a device. Define a neutral band so that 0.49 and 0.51 do not trigger opposite actions, log the raw probability next to every decision for audit, and never let one number take down a link.

One thing this chapter deliberately deferred is the other half of the picture: Choice and Score answers carry a separate confidence field that a Noul does not, and that field has its own thresholds, its own failure modes, and its own tuning discipline. The next chapter takes it up in full.

Post-Quiz: Worked Examples

In the Aruba example, why is authorization_fail_through checked with a LOW-probability comparison (below the review floor) rather than a HIGH-probability comparison like the other two checks?

Because the underlying question was written so a HIGH probability confirms fail-through IS enabled, so proving it is ABSENT requires a LOW probability
Because fail-through is inherently harder for Jev to detect than the other two settings
Because inverted checks are the only case where TypeSafe requires a criteria field
Because the SDK returns fail-through checks as a Choice object instead of a Noul

What role does the maintenance_expected Noul play in the Splunk routing-loop detection code?

It replaces the need for the other three Noul checks entirely
It acts as a suppressor: the page only fires when this probability is low, confirming the activity is not expected maintenance
It is averaged together with the other three signals into a single composite score
It sets the actual severity level passed to page_oncall

In the change-request example, rollback_present returns 0.93 but rollback_executable returns 0.06. What does this pairing demonstrate?

That the Noul battery is unreliable and produced a contradictory result
That a rollback section can exist in the ticket text while being operationally worthless — a distinction a keyword search for "rollback" cannot make
That rollback_present should have been weighted more heavily than rollback_executable
That the two checks should be merged into a single Noul to avoid confusion

Your Progress

Answer Explanations