Decision schema
The JSON document behind every decision — the state it accepts, the questions it answers, composites, the policies that pick the action, the runtime settings and the destinations.
A decision is defined by one JSON document, the decision schema. The editor and the visual builder edit it for you (the right-hand panel shows it under Decision schema), the CLI scaffolds and validates it offline, and every deployed version stores a complete snapshot of it.
The schema is engine-neutral: nothing in it is specific to Jev or any other engine.
A complete example
This is the Lead Qualification template as it is stored, with every default filled in:
{
"stateSchema": {
"kind": "object",
"fields": [
{ "key": "message", "type": "string", "required": true, "description": "Inbound lead message" },
{ "key": "company_size", "type": "number", "required": false, "description": "Employee count" },
{ "key": "source", "type": "string", "required": false, "description": "website, referral, ads…" }
]
},
"questions": [
{
"key": "purchase_intent",
"type": "probability",
"instructions": "Does this lead have real intent to buy in the next 30 days?",
"criteria": {
"yes": "asks for pricing, a demo, a quote or a start date",
"no": "just browsing, student, vendor or spam"
}
},
{
"key": "priority",
"type": "score",
"instructions": "How urgent is it to answer this lead?",
"scale": ["low", "medium", "high", "critical"]
},
{
"key": "route",
"type": "choice",
"instructions": "Which team should receive this lead?",
"options": [
{ "value": "sales", "description": "high intent, ready to talk to sales" },
{ "value": "sdr", "description": "some intent, needs qualification" },
{ "value": "nurture", "description": "early stage, send content" },
{ "value": "spam", "description": "spam, vendor pitch or irrelevant" },
{ "value": "other", "description": "none of the options above" }
]
}
],
"composites": [
{
"key": "lead_score",
"description": "0–1 ranking: intent counts double, priority adds, spam-like routes don't count",
"terms": [
{ "question": "purchase_intent", "weight": 2 },
{ "question": "priority", "weight": 1 },
{ "question": "route", "option": "sales", "weight": 1 }
]
}
],
"policies": [
{ "field": "route", "on": "output", "operator": "eq", "value": "spam", "action": "block" },
{ "field": "purchase_intent", "on": "confidence", "operator": "lt", "value": 0.6, "action": "escalate" }
],
"settings": {
"storeInput": true,
"storeOutput": true,
"timeoutMs": 5000,
"fallbackAction": "escalate",
"onEngineError": "error"
}
}Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
stateSchema | object | Yes | The input the decision accepts — see State schema. |
questions | array | Yes | 1 to 64 questions — see Questions. |
composites | array | No | Up to 20 weighted combinations of answers. Default [] — see Composites. |
policies | array | No | Up to 50 rules, evaluated in order. Default [] — see Policies and actions. |
context | string | No | Up to 8,000 characters of business context sent with every state — see Context. |
settings | object | No | Engine, storage, time budget, fallback action and engine-error behavior — see Decision settings. |
destinations | array | No | Up to 20 destinations — what happens after the decision, per option, level, threshold or final action. Default [] — see Destinations. |
Keys
State fields, question keys, composite keys and choice option values share one rule because they become JSON keys and values in the API response:
- snake_case: start with a lowercase letter, then lowercase letters, digits or
_; - at most 48 characters (pattern
^[a-z][a-z0-9_]{0,47}$); - unique: question and composite keys within the decision, field keys within the state, option values within a question.
State schema
The state is what your application sends in the request body as { "state": … }. Choose one of three kinds — the same three shapes Jev evaluates natively:
| Kind | Send | Good for |
|---|---|---|
object | a JSON object with typed fields | most decisions: named fields keep each part of the input clear |
text | a non-empty string | one message, comment or passage |
list | a non-empty array of strings or objects | a chat transcript, a batch of records |
Object state
{ "kind": "object", "fields": [{ "key": "message", "type": "string", "required": true }] }fields holds up to 50 field definitions:
| Property | Type | Default | Description |
|---|---|---|---|
key | string | — | The JSON key in the state. |
type | string | — | One of string, number, integer, boolean, array, object. |
required | boolean | false | Reject states that don't have this field. |
description | string | — | Up to 255 characters. Documents the field in the API contract shown by the editor. |
How the state is validated before the engine runs:
- the state must be a JSON object (not an array, a string or
null); - a
requiredfield must be present and notnull; an optional field may be missing ornull; - types are strict:
numberis any finite number,integera whole number,objecta non-null object that isn't an array —"500"is not anumber; - fields that aren't declared are accepted and forwarded to the engine, so you can send extra context without changing the schema;
- the first failing field is reported, for example
422 INVALID_STATEwith"Field company_size must be a number.".
Field descriptions are documentation
The engine receives your state's keys and values, the decision's context and the questions — not the field descriptions. Give the engine what it needs through descriptive field names, context and precise instructions, and point instructions at fields by name: "Does `message` ask for a demo?".
Text state
{ "kind": "text" }The state must be a non-empty string: { "state": "Congratulations! You won a $1000 gift card…" }. The engine receives the string itself.
List state
{ "kind": "list", "items": "string" }The state must be a non-empty array of up to 500 items. items sets what each item is:
items | Each item must be | Example state |
|---|---|---|
string (default) | a string | ["Hi!", "My card was charged twice.", "Can you refund one?"] |
object | a JSON object (not null, not an array) | [{ "role": "customer", "text": "My card was charged twice." }, { "role": "agent", "text": "Let me check." }] |
The engine receives the array itself. Errors name the problem: "Field state must be a non-empty array.", "Field state has too many items (612, max 500)." or "Item 2 of state must be an object.".
A list is one state: every question is answered about the list as a whole — "does this conversation ask for a refund?" — not once per item. To classify items independently, send one decision per item, or use an object state such as { "messages": [ … ] } with one question per position ("Is `messages[2]` a complaint?").
Size limits
The serialized state must fit in 128 KB and in about 32,000 tokens (estimated as characters ÷ 4).
Jev also has a token budget for the whole call: about 32,000 tokens for the state plus the longest question and 64,000 tokens for the state plus all questions. Dcision checks both before calling the engine; a state over the limits fails with 422 INVALID_STATE and a message that says which limit was hit — for example "The state plus all questions is too large for one engine call (~65210 tokens, max 64000). Shorten the state or split the decision." See Limits.
Context
context is free text, up to 8,000 characters, sent with every state under the reserved key decision_context — for example "Inbound messages from the pricing page of a B2B SaaS. Prices start at $29/month." Describe the business, not the expected answer.
How the engine receives it:
| State kind | What the engine evaluates when context is set |
|---|---|
object | your object plus a decision_context field |
text, list | { "decision_context": "<context>", "input": <your state> } |
Without context, the engine receives your state unchanged.
decision_context is reserved: when the decision has a context, it replaces a decision_context field your application sends in an object state. Any other field — including one named context — is forwarded as is.
Structured entries
Instructions, choice option descriptions, score levels and probability criteria accept JSON objects or arrays as well as text — Jev reads structure natively. Use it to label the parts of a long question, to pass supporting data, or to give each option a rubric. See Structured instructions and criteria.
Validation
The same rules run everywhere:
-
Editor — validates as you type, shows each issue with a Go to error link and only auto-saves valid drafts.
-
API — saving or deploying an invalid schema fails with
422 INVALID_SCHEMA;detailslists every issue with its path:{ "error": { "code": "INVALID_SCHEMA", "message": "The decision schema is invalid.", "request_id": "req_7Gm2xPq9Lk4sVt1RbN8w", "details": [ { "path": "questions.0.options", "message": "Add at least 2 options (besides \"other\")." }, { "path": "policies.1.field", "message": "Unknown question \"intent\"." }, { "path": "composites.0.terms.2.option", "message": "Pick the option whose probability counts." } ] } } -
CLI —
dcision validate decision.jsonchecks a file offline, anddcision check-statechecks a state against it.
Validation rules can get stricter over time — since v0.3, for example, rule values are checked against the options, levels and ranges they compare with ("Use a number between 0 and 1 (e.g. 0.7, not 70)."). Stricter rules apply when you save or deploy; versions already deployed keep running unchanged, so a tightened rule never turns a live endpoint into an error.
The API contract
The editor's API contract view shows what clients of the decision can rely on: the state as JSON Schema, the shape of each question's answer and the composites.
{
"name": "lead-qualification",
"state": {
"type": "object",
"required": ["message"],
"properties": {
"message": { "type": "string", "description": "Inbound lead message" },
"company_size": { "type": "number", "description": "Employee count" },
"source": { "type": "string", "description": "website, referral, ads…" }
}
},
"questions": {
"purchase_intent": { "type": "probability", "range": [0, 1] },
"priority": { "type": "score", "scale": ["low", "medium", "high", "critical"] },
"route": { "type": "choice", "options": ["sales", "sdr", "nurture", "spam", "other"] }
},
"composites": {
"lead_score": { "type": "number" }
}
}A text state is shown as { "type": "string" } and a list state as { "type": "array", "items": { "type": "string" } } (or "object"). Score scales list the level labels, also for structured levels. composites only appears when the decision has some.
Quickstart
Sign up, create a decision from a template, test it in the Playground, deploy it and call it with cURL, JavaScript or Python — in about five minutes.
Questions
Choice, score and probability questions — structured instructions and criteria, what the API returns for each, weighted levels, the reserved other option and minimum confidence.