---
title: "How to build software with System One"
type: concept
tags: [architecture, design, decomposition, workflow, composition]
created: 2026-09-17
updated: 2026-09-21
confidence: high
sources:
  - raw/docs/concepts__how-to-build-with-system-one.md
jev_version: "jev-1.13.0"
sdk_python: "0.7.1"
summary: "Build a normal software workflow and insert System One only where AI is needed: narrow typed questions, composed in code."
---

# How to build software with System One

> **TL;DR** Keep control flow, deterministic rules, and side effects in code. Break broad judgments into narrow, typed questions with explicit `instructions` and `criteria`. Give each question only the context it needs. Ask many independent questions in one request (they run in parallel), combine the answers with your own weights and thresholds, and route on `confidence`.

## What it is

System One is TypeSafe's model for building **AI-powered software, not agents**. It does not generate code or choose its own next action. It provides AI primitives that embed into software, so code remains in control while the model handles common-sense judgments over unstructured data. (It is also not a replacement for the LLM inside a coding agent — see [[concepts/jev-with-coding-agents]].)

The docs' own summary — build a normal software workflow and insert System One only where AI is needed: keep control flow, deterministic rules and side effects in code; break broad judgments into narrow typed questions with explicit instructions and criteria; give each question only the context it needs; use probabilities and confidence to act, ask for review or escalate; ask independent questions together and compose their answers in code.

### Three software architectures

| Architecture | How it works |
|---|---|
| Traditional software | A decision tree of simple, reliable primitives that developers compose into higher-level abstractions. |
| LLM agents | The agent reads instructions and picks its next step. Fine with a person monitoring, but every loop is another chance to go off the rails. |
| AI-powered software | Code handles deterministic work and owns control flow. The model appears only where the system needs programmable common sense or must interpret unstructured data. Each AI task is kept atomic and constrained. |

### What makes System One composable

| Property | What it buys you |
|---|---|
| **Structured** | Type-safe by construction: decisions and probabilities conform to the types and JSON schema your code expects, so code never recovers a value from prose. |
| **Parallel** | Questions are evaluated independently and in parallel. One primitive's result does not become hidden context that changes another's. |
| **Comparable** | Outputs are sortable and can drive smart `if` statements, thresholds, and comparisons. |
| **Fast** | Most queries complete in about 100 ms — fast enough for real-time request paths and user interfaces. |
| **Calibrated confidence** | RLCD ([[concepts/machine-learning-primer]]) reports uncertainty as calibrated probabilities rather than tending to overconfidence. |
| **Self-consistent** | Designed to return stable answers across repeated evaluations. See [[cookbooks/consistency-noul]]. |

Every output is constrained to the supplied options, so the model returns a full probability distribution over them rather than inventing a value outside the schema. TypeSafe states its target is a **greater than 100× intelligence-to-speed-and-cost ratio**, on the bet that cheaper intelligence creates much more demand.

## The design workflow (7 steps)

### 1. Use code when you can

Keep deterministic work in code: it is reliable and cheap. Avoid agent `while` loops when a software workflow expresses the same behavior.

```python theme={null}
days_overdue = (today - invoice.due_date).days

if days_overdue > 30:
    route_to_collections(invoice)
```

See [[patterns/overview]] for composing model decisions with code.

### 2. Decompose the input state

Include only the context the current questions need: this avoids distractions and context rot. Do not rely on knowledge in the model weights when current information can come from your own knowledge base.

```json title="request" theme={null}
{
  "state": {
    "ticket_message": "My flight was cancelled. Can I get a refund?",
    "refund_policy": "Cancelled flights are eligible for a full refund."
  },
  "questions": {
    "policy_supports_refund": {
      "type": "noul",
      "instructions": "Does the refund policy support the refund requested in the ticket?"
    }
  }
}
```

### 3. Use structure in the input state

Use nested JSON for `state` and `questions`. Point questions at specific values when that removes ambiguity, and **include the backtick characters around each path inside the question** — a dot-and-index path such as `` `support.tickets[0].message` ``.

```json title="request" theme={null}
{
  "state": {
    "support": { "tickets": [
      { "message": "I was charged twice for order A-104." },
      { "message": "How do I reset my password?" }
    ] },
    "commerce": { "orders": [
      { "id": "A-104", "charges": [
        { "amount_usd": 49, "status": "captured" },
        { "amount_usd": 49, "status": "captured" }
      ] }
    ] },
    "account": { "security": {
      "password_reset": "Email a reset link to the address on file."
    } }
  },
  "questions": {
    "duplicate_charge": {
      "type": "noul",
      "instructions": "Do `support.tickets[0].message` and `commerce.orders[0].charges` indicate a duplicate charge?"
    },
    "password_reset_supported": {
      "type": "noul",
      "instructions": "Can `account.security.password_reset` resolve the request in `support.tickets[1].message`?"
    }
  }
}
```

### 4. Decompose the questions

Ask the most explicit, narrow, atomic questions you can. Break complex or ill-defined questions into separate questions that each evaluate one property.

> The docs flag this as "probably the most important concept in this guide. Broad questions hide several judgments behind one answer. Atomic questions expose those judgments so you can inspect, tune, and combine them in code."

**Worked example — spam detection.** The broad question (bad) is a single Noul `is_spam` with `instructions` "Is `message` spam?".

Decomposed (good), over a state whose `message` has `sender.display_name` `"Acme Payroll"`, `sender.email` `"rewards@claim-bonus.example"`, subject `"Urgent: claim your employee bonus"`, a bonus-offer body, and a `links[0]` of text `"Claim bonus"` → `http://claim-bonus.example/acme`. All six are `{"type": "noul", "instructions": …}`:

| question id | `instructions` |
|---|---|
| `requests_credentials` | Does `message.body` ask the recipient to provide a password or other login credential? |
| `offers_unexpected_reward` | Does `message.body` claim the recipient received an unexpected prize, payment, or reward? |
| `creates_time_pressure` | Does `message.subject` or `message.body` pressure the recipient to act quickly? |
| `sender_identity_mismatch` | Does the organization named in `message.sender.display_name` conflict with the domain in `message.sender.email`? |
| `link_domain_mismatch` | Does the domain in `message.links[0].url` conflict with the organization named in `message.sender.display_name`? |
| `disguises_link_destination` | Does `message.links[0].text` conceal or misrepresent the destination in `message.links[0].url`? |

**Worked example — verifying a tool-call trace.** The bad version asks one question, `tool_calls_are_correct`: *"Is `trace.tool_calls` correct for `request` and `available_tools`?"* The good version asks nine, each checking one property of the same state (a `request` for Seattle weather in fahrenheit on `2026-09-03`, an `available_tools` map with `geocode_city` and `get_weather`, and a `trace` where `tool_calls[1].arguments.unit` is `"celsius"`):

All nine are `{"type": "noul", "instructions": …}`:

| question id | `instructions` |
|---|---|
| `geocode_tool_is_relevant` | Is `trace.tool_calls[0].name` an appropriate tool for resolving `request.location`? |
| `geocode_location_matches` | Does `trace.tool_calls[0].arguments.city` match `request.location`? |
| `geocode_arguments_match_schema` | Does `trace.tool_calls[0].arguments` conform to `available_tools.geocode_city.parameters`? |
| `geocode_result_matches_call` | Does `trace.tool_results[0].tool_call_id` match `trace.tool_calls[0].id`? |
| `weather_tool_is_relevant` | Is `trace.tool_calls[1].name` an appropriate tool for answering `request.text`? |
| `weather_arguments_match_schema` | Does `trace.tool_calls[1].arguments` conform to `available_tools.get_weather.parameters`? |
| `weather_uses_geocoded_coordinates` | Do the coordinates in `trace.tool_calls[1].arguments` match those in `trace.tool_results[0].output`? |
| `weather_date_matches` | Does `trace.tool_calls[1].arguments.date` match `request.date`? |
| `weather_unit_matches` | Does `trace.tool_calls[1].arguments.unit` match `request.unit`? |

The payoff: the broad question returns one number that hides the unit mismatch; the decomposed set isolates it in `weather_unit_matches`.

### 5. Use structure in the questions

Keep questions short. `instructions` and `criteria` are usually strings, and for a short, unambiguous question a string is all you need. They can also be objects or arrays: put the question in one field and the data that guides it in the others. Upstream names three cases where structure helps (rewritten 2026-09-21):

- **the question needs context or examples** — background or example inputs go in named fields "where your code can add to them or swap them without rewriting the question";
- **part of the question comes from your code** — "when a value comes from a database, put it in its own field instead of splicing it into a string template";
- **several questions have similar instructions** — the supplementary data is what makes them distinct.

Data a question refers to is named in backticks inside the question text, exactly like a `state` path:

```json
"instructions": {
  "potential_duplicate": { "name": "John Smith", "location": "Oakland, California", "last_employer": "Google" },
  "question": "Is the resume for the same person as `potential_duplicate`?"
}
```


`criteria` descriptions can be objects too. For a Choice, describe **what belongs in each option**, **what belongs in a neighboring option instead** (`not_for`), and a few representative `examples`. Use the same field names across options so the model can compare them directly.

```json title="questions" theme={null}
{
  "card_help_topic": {
    "type": "choice",
    "instructions": {
      "question": "Which disposable virtual card topic is the user asking about?",
      "focus": "Classify the information the user wants."
    },
    "criteria": {
      "get_disposable_virtual_card": {
        "what": "Purpose, eligibility, or setup",
        "not_for": "Quantity, transaction, or merchant restrictions",
        "examples": [
          "How can I get a disposable virtual card?",
          "What are disposable cards for?"
        ]
      },
      "disposable_card_limits": {
        "what": "Quantity, transaction, or merchant restrictions",
        "not_for": "Purpose, eligibility, or setup",
        "examples": [
          "How many disposable cards can I make per day?",
          "Where can I use a disposable card?"
        ]
      }
    }
  }
}
```

(State for that example: the string `"How many disposable virtual cards can I make per day?"`.)

A short, unambiguous question or criterion can remain a string. Worked example per type: [[concepts/noul]] (one database record per question, questions built in code), [[concepts/choice]] (two easily confused options), [[concepts/score]] (a level description plus example situations), and [[cookbooks/sde-cascade]] for the shared-wording case. Full set of places structure is accepted: [[concepts/advanced-structure]].

### 6. Ask a lot of questions

Ask many narrow, independent questions about the same state in one request: that is how you maximize intelligence per dollar. Questions run in parallel, and code combines their signals without serial round trips. See [[patterns/fan-out]] and [[cookbooks/parallel-questions]].

### 7. Combine outputs in code, then route on uncertainty

Combine independent answers with deterministic rules or weighted sums. For learned composition, use the probabilities as features in a downstream classical ML model.

```python theme={null}
answers = response.answers

# Combine independent signals into one application-specific score.
quality = (
    0.4 * answers["answers_request"].noul
    + 0.4 * answers["citations_are_supported"].noul
    + 0.2 * (1 - answers["contradicts_context"].noul)
)
```

Take different actions for confident and unconfident answers. Escalate uncertain cases to a person or a more expensive reasoning model. Test thresholds by plotting confidence against accuracy on your data.

```python theme={null}
answer = response.answers["card_help_topic"]

if answer.confidence < 0.8:
    route_to_human_review(ticket)
else:
    route_to_handler(answer.choice, ticket)
```

See [[patterns/composite-scoring]] for preserving individual judgments while combining them, [[cookbooks/autoresearch-feature-discovery]] for training a classical model on System One outputs when you lack labels, and [[patterns/confidence-routing]] for matching thresholds to each action's risk.

> **Tip from the docs:** decomposition does not require more round trips. Questions over the same state run in parallel.

## Putting it all together

The full worked example from the docs: a support-ticket triage that keeps deterministic work in code, sends only relevant structured context, evaluates many atomic questions in one request, and composes the answers with explicit confidence gates.

```python title="triage_ticket.py" theme={null}
from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient


def triage_ticket(ticket, customer):
    # Handle deterministic states without calling a model.
    if ticket["status"] == "closed":
        return "no_action"

    open_orders = [
        order for order in customer["orders"] if order["status"] != "delivered"
    ]

    # Include only the structured context needed by the questions below.
    state = {
        "ticket": {
            "message": ticket["message"],
            "sender": ticket["sender"],
            "links": ticket["links"],
        },
        "customer": {
            "plan": customer["plan"],
            "open_orders": open_orders,
        },
        "policy": {
            "sensitive_credentials": ["password", "security code", "API key"],
        },
    }

    # Ask structured, atomic questions together so they run in parallel.
    questions = {
        "topic": Choice(
            instructions={
                "question": "Which team should handle `ticket.message`?",
                "focus": "Classify the customer's primary request.",
            },
            criteria={
                "billing": {
                    "what": "Charges, invoices, refunds, or subscriptions",
                    "not_for": "Order tracking or account access",
                    "examples": ["I was charged twice", "Where is my refund?"],
                },
                "orders": {
                    "what": "Order status, delivery, cancellation, or returns",
                    "not_for": "Charges or account access",
                    "examples": ["Where is my order?", "Cancel my shipment"],
                },
                "account": {
                    "what": "Login, profile, permissions, or security",
                    "not_for": "Charges or order tracking",
                    "examples": ["Reset my password", "I cannot sign in"],
                },
            },
        ),
        "requests_credentials": Noul(
            instructions={
                "question": "Does the message request a sensitive credential?",
                "compare": [
                    "`ticket.message`",
                    "`policy.sensitive_credentials`",
                ],
                "focus": "Look for a request to disclose the credential itself.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Asks the recipient to disclose a listed credential",
                    "examples": [
                        "Reply with your password",
                        "Send us your API key",
                    ],
                },
                false={
                    "what": "Does not ask the recipient to disclose a credential",
                    "not_for": "A legitimate instruction to reset a credential",
                    "examples": ["Use this link to reset your password"],
                },
            ),
        ),
        "sender_identity_mismatch": Noul(
            instructions={
                "question": "Does the claimed sender identity conflict with its domain?",
                "compare": [
                    "`ticket.sender.display_name`",
                    "`ticket.sender.email`",
                ],
                "focus": "Compare the named organization with the email domain.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Claims an organization unrelated to the email domain",
                    "examples": ["Acme Payroll sent from claim-bonus.example"],
                },
                false={
                    "what": "The identity and domain agree or make no conflicting claim",
                    "examples": ["Acme Payroll sent from acme.example"],
                },
            ),
        ),
        "unexpected_reward": Noul(
            instructions={
                "question": "Does the message announce an unexpected reward?",
                "inspect": "`ticket.message`",
                "focus": "Look for an unsolicited prize, payment, or reward claim.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Announces an unrequested prize, payment, or reward",
                    "examples": ["You were selected for a $1,000 bonus"],
                },
                false={
                    "what": "Contains no reward claim or discusses an expected payment",
                    "not_for": "A customer asking about a known refund or payroll deposit",
                    "examples": ["When will my approved refund arrive?"],
                },
            ),
        ),
        "refund_requested": Noul(
            instructions={
                "question": "Does the customer explicitly request a refund or credit?",
                "inspect": "`ticket.message`",
                "focus": "Require a requested remedy, not a billing complaint alone.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Directly asks for money back or an account credit",
                    "examples": ["Please refund the duplicate charge"],
                },
                false={
                    "what": "Does not ask for a refund or credit",
                    "not_for": "A complaint or billing question without a requested remedy",
                    "examples": ["Why was I charged twice?"],
                },
            ),
        ),
        "mentions_open_order": Noul(
            instructions={
                "question": "Does the message refer to a supplied open order?",
                "compare": [
                    "`ticket.message`",
                    "`customer.open_orders`",
                ],
                "focus": "Match an order id or other identifying details.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Refers to an open order by id or identifying details",
                    "examples": ["Where is order A-104?"],
                },
                false={
                    "what": "Does not identify any supplied open order",
                    "not_for": "A generic order question with no matching details",
                    "examples": ["How long does shipping usually take?"],
                },
            ),
        ),
        "frustration": Score(
            instructions={
                "question": "How frustrated does the customer appear?",
                "inspect": "`ticket.message`",
                "focus": "Judge expressed frustration, not issue severity.",
            },
            criteria=[
                {
                    "what": "Calm and matter-of-fact",
                    "signals": ["Neutral wording", "No complaint about the experience"],
                },
                {
                    "what": "Frustrated but civil",
                    "signals": ["Expresses annoyance", "Remains constructive"],
                },
                {
                    "what": "Very angry or threatening to leave",
                    "signals": ["Hostile language", "Threatens cancellation or churn"],
                },
            ],
        ),
    }

    with TypeSafeClient() as client:
        response = client.system_one(
            state=state,
            questions=questions,
        )

    # Compose independent spam signals with weights controlled by code.
    answers = response.answers
    spam_risk = (
        0.45 * answers["requests_credentials"].noul
        + 0.30 * answers["sender_identity_mismatch"].noul
        + 0.25 * answers["unexpected_reward"].noul
    )

    # Escalate uncertain judgments instead of guessing.
    spam_is_uncertain = 0.4 < spam_risk < 0.6
    if spam_is_uncertain or answers["topic"].confidence < 0.75:
        return route_to_human_review(ticket)
    if spam_risk >= 0.6:
        return quarantine_as_spam(ticket)

    # Let code decide which speculative answers matter on this path.
    if answers["topic"].choice == "billing":
        return route_to_billing(
            ticket,
            refund_requested=answers["refund_requested"].noul >= 0.7,
        )
    if answers["topic"].choice == "orders":
        return route_to_orders(
            ticket,
            mentions_open_order=answers["mentions_open_order"].noul >= 0.7,
        )

    priority = (
        "high"
        if answers["frustration"].confidence >= 0.7
        and answers["frustration"].score >= 1.5
        else "normal"
    )
    return route_to_account_support(ticket, priority=priority)
```

Contracts visible in that example, worth memorizing:

- Imports come from `typesafe_sdk` (`Choice`, `Noul`, `NoulCriteria`, `Score`, `TypeSafeClient`); `TypeSafeClient()` is a context manager; the call is `client.system_one(state=..., questions=...)`, and `questions` is a dict keyed by your own names whose answers return under the same keys via `response.answers[...]`.
- Value accessors per type: `.noul` (float 0–1), `.choice` (the option key), `.score` (float), and `.confidence` on Choice and Score.
- `Noul` criteria use `NoulCriteria(true=..., false=...)`; `Choice` criteria are a dict keyed by option name; `Score` criteria are an **ordered list**, lowest level first (the frustration example runs calm → frustrated → very angry). In the Python SDK 0.7.1 `Score.criteria` is an ordered sequence, not an int-keyed dict.
- Structured `instructions` fields used in the source: `question`, `focus`, `inspect`, `compare`. Structured criteria fields: `what`, `not_for`, `examples`, `signals`.

## Gotchas

- **Speculative answers are nearly free in latency.** The example asks `refund_requested` and `mentions_open_order` even though only one branch will use them — that is the intended pattern, not waste. Upstream softened the claim on 2026-09-21 from "typically doesn't add any latency" to "usually has little effect on response time"; extra questions still cost tokens.
- **Two different uncertainty gates.** `spam_is_uncertain` thresholds a *derived* band (`0.4 < spam_risk < 0.6`); `answers["topic"].confidence < 0.75` thresholds the model's own confidence. Noul answers carry no `confidence` ([[concepts/confidence]]).
- **`not_for` is doing real work.** Contrastive criteria (what belongs here vs. the neighboring option) are what make a Choice boundary crisp.
- **Backticks matter.** The docs require the backtick characters around a nested path inside the question text.
- **Do not let a broad question hide a compound judgment.** If you cannot name exactly one property it tests, it is not atomic yet.

## Related

- [[concepts/system-one]] — what the model is
- [[concepts/state]] — building the input
- [[concepts/primitives]] — Choice, Score, Noul
- [[concepts/advanced-structure]] — structured instructions, options, levels, criteria
- [[concepts/confidence]] — thresholds and escalation
- [[patterns/fan-out]] — asking many questions at once
- [[patterns/composite-scoring]] — combining answers
- [[patterns/confidence-routing]] — risk-matched gates
- [[cookbooks/parallel-questions]] — parallel questions in practice
- [[reference/python-sdk]] — client and `system_one()` signature

## Sources

- raw/docs/concepts__how-to-build-with-system-one.md (https://docs.typesafe.ai/concepts/how-to-build-with-system-one)
