Apex-1 Decisions
Apex-1 is SCX.ai's System One decision model, served as model apex. You send it a state, the text or record you want judged, and a set of named questions. It returns one typed answer per question: the probability of yes, the most likely of your options, or a level on your scale, each with the probabilities behind it.
Nothing is generated. Every answer is one of the options you gave, your code never parses prose, and you pay for input tokens only. Use Apex-1 wherever you would otherwise prompt a chat model and parse its reply:
- Triage and routing: send tickets, emails and chat messages to the right team, workflow, specialist model or person.
- Moderation and guardrails: screen what goes into and comes out of an LLM app.
- Checks: does a reply answer the question, does a citation support the claim, does a receipt match the expense claim.
- Scoring and ranking: leads, applications, search results and retrieved passages, on rubrics you define.
If you have used TypeSafe's Jev, the request and response have the same shape. The API reference lists the differences at the end.
Your first request
Describe the state and the questions
Save this request as triage.json. The state is one support message. The five questions ask which team should handle it, whether it is urgent, whether it asks for a refund, how severe the problem is and how frustrated the customer sounds.
{
"model": "apex",
"state": {
"message": "Our checkout has returned a 500 error on every order since 9am, so we can't take any payments. Please fix this today.",
"customer_plan": "Business"
},
"questions": {
"team": {
"type": "choice",
"instructions": "Which team should handle `message`?",
"criteria": {
"billing": "charges, invoices, refunds or subscriptions",
"technical": "errors, outages or integration problems",
"account": "logging in, access or account details",
"other": "anything else"
}
},
"urgent": {
"type": "noul",
"instructions": "Does `message` say the problem must be fixed today?"
},
"refund_requested": {
"type": "noul",
"instructions": "Does `message` explicitly ask for a refund or a credit?"
},
"severity": {
"type": "score",
"instructions": "How severe is the problem described in `message`?",
"criteria": [
"cosmetic: nothing is broken",
"minor: a workaround exists",
"major: a feature is broken",
"critical: the customer cannot operate"
]
},
"frustration": {
"type": "score",
"instructions": "How frustrated does the customer sound in `message`?",
"criteria": ["calm", "annoyed but civil", "frustrated", "angry or threatening to leave"]
}
}
}
{
"model": "apex",
"state": {
"message": "Our checkout has returned a 500 error on every order since 9am, so we can't take any payments. Please fix this today.",
"customer_plan": "Business"
},
"questions": {
"team": {
"type": "choice",
"instructions": "Which team should handle `message`?",
"criteria": {
"billing": "charges, invoices, refunds or subscriptions",
"technical": "errors, outages or integration problems",
"account": "logging in, access or account details",
"other": "anything else"
}
},
"urgent": {
"type": "noul",
"instructions": "Does `message` say the problem must be fixed today?"
},
"refund_requested": {
"type": "noul",
"instructions": "Does `message` explicitly ask for a refund or a credit?"
},
"severity": {
"type": "score",
"instructions": "How severe is the problem described in `message`?",
"criteria": [
"cosmetic: nothing is broken",
"minor: a workaround exists",
"major: a feature is broken",
"critical: the customer cannot operate"
]
},
"frustration": {
"type": "score",
"instructions": "How frustrated does the customer sound in `message`?",
"criteria": ["calm", "annoyed but civil", "frustrated", "angry or threatening to leave"]
}
}
}
Send it
import json
import requests
with open("triage.json") as f:
body = json.load(f)
response = requests.post(
"https://api.scx.ai/v1/systemone",
headers={"Authorization": "Bearer your-scx-api-key"},
json=body,
timeout=120,
)
response.raise_for_status()
answers = response.json()["answers"]
print(answers["team"]["choice"], answers["urgent"]["noul"], answers["severity"]["score"])
import json
import requests
with open("triage.json") as f:
body = json.load(f)
response = requests.post(
"https://api.scx.ai/v1/systemone",
headers={"Authorization": "Bearer your-scx-api-key"},
json=body,
timeout=120,
)
response.raise_for_status()
answers = response.json()["answers"]
print(answers["team"]["choice"], answers["urgent"]["noul"], answers["severity"]["score"])
Read the answers
Each answer comes back under the name you gave its question. Numbers are rounded here.
{
"id": "so1_3f6a2c1e-8b4d-4e0f-9a7c-5d2e1b0c9f84",
"model": "apex",
"answers": {
"team": {
"type": "choice",
"choice": "technical",
"confidence": 0.926,
"probabilities": { "billing": 0.038, "technical": 0.926, "account": 0.002, "other": 0.035 }
},
"urgent": { "type": "noul", "noul": 0.997 },
"refund_requested": { "type": "noul", "noul": 0.004 },
"severity": {
"type": "score",
"score": 2.958,
"confidence": 0.962,
"probabilities": { "0": 0.001, "1": 0.002, "2": 0.034, "3": 0.962 }
},
"frustration": {
"type": "score",
"score": 1.121,
"confidence": 0.49,
"probabilities": { "0": 0.216, "1": 0.49, "2": 0.252, "3": 0.042 }
}
},
"usage": { "input_tokens": 337, "output_tokens": 0 }
}
{
"id": "so1_3f6a2c1e-8b4d-4e0f-9a7c-5d2e1b0c9f84",
"model": "apex",
"answers": {
"team": {
"type": "choice",
"choice": "technical",
"confidence": 0.926,
"probabilities": { "billing": 0.038, "technical": 0.926, "account": 0.002, "other": 0.035 }
},
"urgent": { "type": "noul", "noul": 0.997 },
"refund_requested": { "type": "noul", "noul": 0.004 },
"severity": {
"type": "score",
"score": 2.958,
"confidence": 0.962,
"probabilities": { "0": 0.001, "1": 0.002, "2": 0.034, "3": 0.962 }
},
"frustration": {
"type": "score",
"score": 1.121,
"confidence": 0.49,
"probabilities": { "0": 0.216, "1": 0.49, "2": 0.252, "3": 0.042 }
}
},
"usage": { "input_tokens": 337, "output_tokens": 0 }
}
| Type | Read it as |
|---|---|
noul | noul is the probability of yes, from 0 to 1. urgent at 0.997 is a firm yes and refund_requested at 0.004 a firm no. |
choice | choice is the most likely option and confidence is its probability. probabilities covers every option and sums to 1. |
score | score is the expected level, from 0 to the number of levels minus 1, so it can fall between levels. severity at 2.96 of 3 is "critical". confidence is the probability of the single most likely level. |
frustration is spread across calm, annoyed and frustrated: a score of 1.12 with a confidence of 0.49. Treat that as "unsure", not as "annoyed".
Write good questions
- Ask for one quick judgment per question. Ask what a knowledgeable person could answer in a second, given the right context. "Does
messagesay the problem must be fixed today?" works. "Analyse this ticket and decide what to do" does not. Split a broad judgment into narrow questions and combine the answers in code. - Choose the type by the shape of the answer.
choicepicks one of a fixed set of unordered options, such as a team, a document type or an intent. Add anotheroption when your list might not cover every input.scoreplaces the state on a scale you can describe level by level, such as severity, frustration or fit. List the levels lowest first.noulanswers a clear yes/no condition, such as "asks for a refund" or "contains a phone number".- If you want to ask "is this strong?", describe the levels and use a
score. Anoulof 0.5 means "unsure", not "medium".
- Write the whole question in
instructions. The question's name is for your code, so don't rely on it to carry meaning. Leaveinstructionsout only when the criteria say it all. - Be literal and specific. Apex-1 answers the question you wrote, not the one you meant. State the exact condition, and put the boundary cases in the criteria, as in "explicitly ask for a refund or a credit". Keep the instructions and criteria in agreement: don't describe
trueas the "no" outcome. - Point at the field. When the state is an object, name the key in the question, in backticks: "Which team should handle
message?"
Shape the state
- Use a string for a single text, an array for a list such as a conversation, and an object for most things: one key per piece of context, named as you would label it for a person, because the model reads the keys too.
- Send only what the questions need. Irrelevant detail makes answers less accurate, and every question is billed for the whole state. Filter, look up and trim in code first.
Ask several questions at once
Every question in a request sees the same state and is answered on its own, so adding a question adds little time. Ask the speculative questions too. severity only matters for technical tickets and refund_requested only for billing, but asking them in the same request saves a second round trip, and your code ignores the answers it doesn't need.
Plan for two things:
- The state is billed once per question. The request above billed 337 input tokens: the short state read five times, plus the five questions. With a large state, keep each request to the questions that need it, and send unrelated questions in separate requests with smaller states.
- Answers don't see each other. If a question depends on another's answer, for example because you need that answer to fetch more data or to choose the next options, make a second request.
Act on the answers in code
Answers are numbers your code can compare, threshold and combine. Keep the decisions, and the risk you accept, in your code.
- Threshold nouls. Start with yes above 0.7 and no below 0.3, treat the middle as uncertain, and tune on your own data.
- Gate on confidence.
confidenceis the probability of the chosen option or level. A random guess scores 1 divided by the number of options, so set a floor per question, and set it higher for actions that are hard to undo. - Escalate what Apex-1 is unsure of. Send low-confidence cases to a person or a reasoning model. Code handles the clear cases cheaply, and the expensive handlers see only the hard ones.
Sent the same questions, "Since your update yesterday I can't open my invoices page. Is that something on your end?" comes back with billing at 0.47 and technical at 0.44. The gate below sends it to a person instead of guessing:
def route(answers):
team = answers["team"]
if team["confidence"] < 0.6:
return "human_review"
if team["choice"] == "technical":
critical = answers["severity"]["score"] >= 2.5
urgent = answers["urgent"]["noul"] > 0.7
return "page_on_call" if critical and urgent else "technical_queue"
if team["choice"] == "billing":
return "refunds" if answers["refund_requested"]["noul"] > 0.7 else "billing_queue"
return f"{team['choice']}_queue"
def route(answers):
team = answers["team"]
if team["confidence"] < 0.6:
return "human_review"
if team["choice"] == "technical":
critical = answers["severity"]["score"] >= 2.5
urgent = answers["urgent"]["noul"] > 0.7
return "page_on_call" if critical and urgent else "technical_queue"
if team["choice"] == "billing":
return "refunds" if answers["refund_requested"]["noul"] > 0.7 else "billing_queue"
return f"{team['choice']}_queue"
Combine scores with weights you own. Normalise each score to 0–1 by dividing by the number of levels minus 1, then weight them. When the ranking doesn't match what your team would decide, you change a weight, not a prompt.
def scaled(answer, levels):
return answer["score"] / (levels - 1)
priority = 0.7 * scaled(answers["severity"], 4) + 0.3 * scaled(answers["frustration"], 4)
def scaled(answer, levels):
return answer["score"] / (levels - 1)
priority = 0.7 * scaled(answers["severity"], 4) + 0.3 * scaled(answers["frustration"], 4)
When you need a single level rather than a position between levels, take the most likely one: int(max(probs, key=probs.get)), where probs is the answer's probabilities.
Images
Apex-1 reads PNG and JPEG images. Put each image in assets under a name that can't occur in your text, such as asset:receipt. Write that name wherever the image belongs: in the state, where every question sees it, or in one question's instructions, where only that question does. Each copy is billed. An image request must also send the header TypeSafe-Danger-Zone-Images: true.
{
"model": "apex",
"state": { "receipt": "asset:receipt", "claimed_total": "AUD 84.20" },
"questions": {
"total_matches": {
"type": "noul",
"instructions": "Does the total printed on the receipt equal the claimed total?"
}
},
"assets": {
"asset:receipt": { "type": "image", "format": "png", "data_base64": "iVBORw0KGgo..." }
}
}
{
"model": "apex",
"state": { "receipt": "asset:receipt", "claimed_total": "AUD 84.20" },
"questions": {
"total_matches": {
"type": "noul",
"instructions": "Does the total printed on the receipt equal the claimed total?"
}
},
"assets": {
"asset:receipt": { "type": "image", "format": "png", "data_base64": "iVBORw0KGgo..." }
}
}
The API reference has the size and format rules, under Images.
Keep in code what code does better
- Arithmetic, counting and date comparisons belong in code. Ask Apex-1 for the parts that need judgment, such as which amount is the total or which month a date names, and compute the rest yourself.
- Test your important
choicequestions by reordering the options and checking that the answer holds. - Treat the state as untrusted input. Text written to argue for its own classification can move an answer. Test adversarial examples before a gate goes live.
- Apex-1 doesn't write text. When you need a reply, pair it with a chat model, and let Apex-1 decide when to call it.
Limits at a glance
| Limit | Value |
|---|---|
| Billing | Input tokens only, with the state counted once per question. usage.input_tokens is the amount billed. |
| Context | 32,768 tokens per question (the state plus that question) and 65,536 per request (the state once plus every question) |
| Options | 1 to 255 per choice, 1 to 10 levels per score |
| Request body | 32 MiB, JSON only |
| Timeout | We wait up to 90 seconds for an answer, so set your client's timeout above that |
| Retries | Retry a 503 with backoff, honouring Retry-After when it's sent |
The System One API reference has every field, error and limit. Rate and spend limits are in Endpoints & Rate Limits.