Development

A Yes, No, Not Applicable Answer Schema for Agent Tool Results

Antonio Evans7 min read

A clean side-by-side diagram on a white background.
Key points

Key points

  1. 1Give every tool output field three explicit states: yes (a true value), no (a real false or empty result), and not applicable (the field does not apply here).
  2. 2Never encode not applicable as false; use null with a sibling status field, or a sentinel object, so a parser can tell "false" from "no answer".
  3. 3Enforce the shape with a JSON Schema and a schema-constrained generation path, then validate at runtime because schema validity does not guarantee value correctness.
  4. 4Add tests that assert all three states explicitly, because a boolean field silently defaulting to false is the failure mode nobody catches until it matters.

Why a boolean field is a trap for tool output

When a tool returns JSON, the easy design is to make every field that could be yes or no a JSON boolean. JSON booleans accept exactly two values, true and false, and values that merely evaluate to true or false, such as 1 and 0, are rejected by a {"type": "boolean"} schema (JSON Schema boolean reference). That precision is the problem: a boolean has no room for "I do not know" or "this does not apply."

Consider a user lookup tool. A model asks for a user and the tool returns {"active": false}. Does that mean the user exists and is deactivated, or that the lookup failed, or that the caller was not permitted to read the active flag? All three collapse into the same token. Downstream code cannot recover the difference, and a model reading the result will invent one.

This matters because structured outputs are the default in production pipelines. Schema-constrained generation guarantees syntactic validity and reduces parsing errors compared with prompted JSON, but it does not ensure value correctness, so teams are advised to pair constrained generation with runtime validation and a fallback path (MLflow on structured outputs). The yes, no, not applicable pattern is a way to make value correctness checkable.

A concrete failure rate helps. When DataChain tested Anthropic's Claude with a structured prompt that asked for JSON, it found roughly 14 to 20 percent of requests returned the JSON prefixed by commentary, which broke parsing (DataChain on enforcing JSON outputs). Format failures get noticed because they are loud. Silent semantic failures, where false means three different things, do not.

Three states, one field at a time

The pattern is simple to state. For each field a tool reports, you define exactly three outcomes and give each one a distinct representation.

StateMeaningRepresentationExample value
YesA real true, or a value is presentValue plus status "present"{"value": true, "status": "present"}
NoA real false, or a genuinely empty resultValue plus status "present"{"value": false, "status": "present"}
Not applicableField does not apply in this contextNull value plus status "not_applicable", or a sentinel object{"value": null, "status": "not_applicable", "reason": "no_permission"}

The key rule: not applicable is never false and never an empty string. It is a separate, named state with a reason attached. The reason matters because it feeds the next decision. If a model knows the field was unavailable because of permission, it can ask for access; if it knows the field was out of scope, it can drop the question.

Two encodings work well. The sibling status field is the easiest to read:

{
  "user_id": "u_8812",
  "email_verified": { "value": true, "status": "present" },
  "phone_verified": { "value": false, "status": "present" },
  "billing_country": { "value": null, "status": "not_applicable", "reason": "out_of_scope" }
}

The sentinel object is useful when you want the value and the status in one place with a fixed shape, which keeps every property the same type across the response and makes schema constraints simpler. Pick one shape per tool and never mix.

The reason is that a model parsing a tool result should never have to infer which kind of "no" it is seeing. As the buyer-focused evaluation checklist from Viktor puts it, a client wants a concrete description of a failure surfaced in the channel where the request was made, with enough detail to debug (Viktor evaluation checklist). A reason code on a not applicable state is exactly that detail for a missing field.

Deciding when a field is not applicable

The hard part is not encoding; it is choosing the state. Three conditions put a field into not applicable rather than no.

Data is missing because the tool could not fetch it. The record exists but the upstream service timed out. Returning false would lie, so return not applicable with reason "fetch_failed". A separate retry or fallback can then act on that reason.

The field is disallowed by authorization. The caller authenticated but the token lacks the scope to read the field. Return not applicable with reason "no_permission". Do not return false; false implies a real negative answer you were allowed to observe.

The field is out of scope for the request. The user asked about account status only, so the tool did not query billing. Return not applicable with reason "not_requested". This is different from fetch_failed because there is nothing to retry.

Everything else is a real value. If the user exists, is active, and is not an admin, the admin flag is present with value false, status present.

A useful decision rule: a field is not applicable when any of the following is true, and no otherwise.

  1. The answer would require data the tool did not and should not have called.
  2. The caller is not permitted to see the answer.
  3. The field does not exist for this kind of entity (an individual user has no parent team).

If none of those apply and the value is genuinely false or empty, it is a no. This is the same reasoning a reviewer applies when a code review checklist item does not apply to that change: the Turing Way guidance explicitly says some items may not apply to your project or language, in which case they should be disregarded (Turing Way code review checklist). You would not mark a language-specific checkbox as "failed" just because it was irrelevant. Tool output deserves the same honesty.

Encoding the states in a small validator

The next step is to make the three states enforceable. Start with a JSON Schema that gives the status an enum, so a model cannot return a fourth invented value. Then add a small validator that also checks the value and status agree.

STATUS = {"present", "not_applicable"}
REASONS = {"fetch_failed", "no_permission", "not_requested", "out_of_scope"}

def validate_field(name, field):
    errors = []
    if field["status"] not in STATUS:
        errors.append(f"{name}: bad status {field['status']!r}")
    if field["status"] == "not_applicable":
        if field.get("value") is not None:
            errors.append(f"{name}: n/a must have null value")
        if field.get("reason") not in REASONS:
            errors.append(f"{name}: n/a needs a known reason")
    else:
        if "reason" in field and field["reason"] is not None:
            errors.append(f"{name}: present field should not carry a reason")
    return errors

Run this over every field after generation. It catches the specific bug the schema cannot: a field that says not applicable but still carries a value, or a present field that carries a permission reason. The schema guarantees shape; the validator guarantees the three states mean what they say.

Schema-constrained generation is the right default, with prompts and regex as a fallback only, because the runtime validator is what catches value errors that schema enforcement lets through (MLflow on structured outputs). Keep the schema minimal, with required fields, strict types, and enums, because that is what makes validation at scale workable (Agenta on structured outputs and function calling).

Writing tests that assert all three states

A tool result is only as good as the test that can prove it. Write one test per state per field, and at least one test that distinguishes the three reasons a field can be not applicable. The shape of the assertions:

def test_phone_verified_false_is_a_no(client):
    out = client.user("u_8812")
    assert out["phone_verified"]["status"] == "present"
    assert out["phone_verified"]["value"] is False

def test_billing_country_missing_permission(client):
    out = client.user("u_8812", scopes=["basic"])
    field = out["billing_country"]
    assert field["status"] == "not_applicable"
    assert field["reason"] == "no_permission"
    assert field["value"] is None

def test_no_field_ever_defaults_to_false(client):
    out = client.user("u_8812", scopes=["basic"])
    for name, field in out.items():
        if name == "user_id":
            continue
        assert field["status"] in {"present", "not_applicable"}
        if field["status"] == "not_applicable":
            assert field["value"] is None

The third test is the one that prevents regressions over time. It is the machine version of the checklist habit Roussy describes: make sure the important thing has been addressed because the list is non-trivial (Marc Roussy code review checklist). Here the important thing is that no field ever silently becomes false.

Run tests at the component level and end to end. Viktor distinguishes deterministic graders (what a script can verify) from LLM-as-a-judge graders (open-ended scoring) and notes that builders need both kinds of evaluation, because a right answer reached through a wrong step breaks on the next run (Viktor evaluation checklist). Your validator is the deterministic grader for state correctness; the judge is for prose quality and should never be asked whether a field is "really" not applicable.

For evaluation of model output more broadly, note the caution that LLM judges drift between runs and can be manipulated by instructions embedded in the text they grade (SoK on grading open-text legal answers). A field's three-state status should never depend on a judge; it should depend on the tool's own logic and the validator.

A worked example with numbers

Suppose a support tool returns a single response for one ticket, with four fields: resolved, refund_issued, customer_vip, and account_manager. Four fields times three possible states is 12 test cases, but the three not applicable reasons collapse some of them. Write the table first.

FieldYesNoNot applicable reason (most likely)
resolvedvalue truevalue falsenot_requested
refund_issuedvalue truevalue falseno_permission
customer_vipvalue truevalue falsefetch_failed
account_managervalue truevalue falseout_of_scope (individual account, no manager)

Now you can assert the count. In the test suite, require that every field in the response is either present or not applicable with a reason from the closed set, and that the response contains at least one present field. Then add a routing test: given the field statuses, the agent's next step is decided.

  • If account_manager is out of scope, the agent does not ask a human manager to approve anything.
  • If refund_issued has reason no_permission, the agent asks the caller to grant scope and does not retry.
  • If customer_vip has reason fetch_failed, the agent retries once and then drops VIP routing.

This is where the pattern earns its keep. A plain {"account_manager": false} would make the agent treat the account as having no manager and skip escalation, which is wrong for a business account. The three-state encoding makes the next action depend on the reason, not the boolean.

Expected pass rate matters too. If your schema validation rejects 3 percent of tool calls, that is a budget for retries and fallbacks. Set a metric for validation failure rate and alert on it, because structured output regressions are otherwise silent (MLflow on structured outputs). A spike in not_applicable with reason fetch_failed is an upstream outage; a spike with reason no_permission is a scope misconfiguration.

Quanta as the execution layer

The pattern above is portable, but the pipeline has a lot of moving parts: tool calls, schema-constrained generation, validation, retries, and evals across models. Quanta Inference lets a team call text, image, video, audio, and embedding models through one OpenAI-compatible API key, which simplifies running the same validator test against several models without separate SDKs (Quanta Inference). The other lever most teams want is skills a coding agent can install to generate the schema, validator, and tests together (Quanta Skills).

FAQ

FAQ

Is not applicable just null?

No. Null alone does not say whether the field was missing, forbidden, or out of scope. Use null as the value for a not applicable field only when a sibling status field says so and a reason code explains which kind of not applicable it is. A bare null with no status cannot be tested precisely and cannot drive a retry decision.

How is this different from a tri-state enum?

It is the same idea applied at the artifact boundary. A tri-state enum inside your code has three values with no attached context. Applied to tool output, the pattern adds a reason code for the not applicable state, which is what lets a model pick a next step and what lets a test assert the specific kind of missing data. The enum plus reason is the full schema.

What if a boolean is a poor fit for my field?

Then give the field three explicit values in the schema instead of a boolean: for example "yes", "no", "not_applicable" as an enum alongside a reason. The important part is not the word yes or no, it is that not applicable exists as a distinct value with a reason. JSON Schema enums make that enforceable while keeping the field type fixed.

How do I stop the model from inventing a fourth state?

Give the status an enum and reject any value outside it, then run a validator that enforces the value and status pairing. Schema-constrained generation reduces but does not eliminate value errors, so runtime validation is still required (MLflow on structured outputs). Log raw and parsed outputs so a violation is visible on the first call.

Do I need an LLM judge to evaluate whether a field is not applicable?

No. Whether a field is applicable is a property of the request and the tool's own permissions and calls, so it belongs in deterministic code and tests. Reserve LLM judges for open-ended quality, where rubric design and calibration matter, and remember they are known to drift and to favour some answer styles over others (survey on LLM-as-a-judge). The three-state status is exactly the kind of fact a judge should never be asked to decide.

Sources

Sources

  1. Boolean
  2. Enforce, Validate, Observe: 3 LLM Structured Output ...
  3. Enforcing JSON Outputs in Commercial LLMs - DataChain
  4. How to Evaluate an AI Agent: 8-Question Checklist (2026)
  5. Checklist for code review process - The Turing Way
  6. The guide to structured outputs and function calling with ...
  7. Ultimate Code Review Checklist - Marc Roussy
  8. SoK: Grading Open-Text Legal Answers, from Law School ...
  9. A survey on LLM-as-a-judge
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles