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:

decision.json
{
  "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

FieldTypeRequiredDescription
stateSchemaobjectYesThe input the decision accepts — see State schema.
questionsarrayYes1 to 64 questions — see Questions.
compositesarrayNoUp to 20 weighted combinations of answers. Default [] — see Composites.
policiesarrayNoUp to 50 rules, evaluated in order. Default [] — see Policies and actions.
contextstringNoUp to 8,000 characters of business context sent with every state — see Context.
settingsobjectNoEngine, storage, time budget, fallback action and engine-error behavior — see Decision settings.
destinationsarrayNoUp 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:

KindSendGood for
objecta JSON object with typed fieldsmost decisions: named fields keep each part of the input clear
texta non-empty stringone message, comment or passage
lista non-empty array of strings or objectsa chat transcript, a batch of records

Object state

{ "kind": "object", "fields": [{ "key": "message", "type": "string", "required": true }] }

fields holds up to 50 field definitions:

PropertyTypeDefaultDescription
keystring—The JSON key in the state.
typestring—One of string, number, integer, boolean, array, object.
requiredbooleanfalseReject states that don't have this field.
descriptionstring—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 required field must be present and not null; an optional field may be missing or null;
  • types are strict: number is any finite number, integer a whole number, object a non-null object that isn't an array — "500" is not a number;
  • 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_STATE with "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:

itemsEach item must beExample state
string (default)a string["Hi!", "My card was charged twice.", "Can you refund one?"]
objecta 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 kindWhat the engine evaluates when context is set
objectyour 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; details lists 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.json checks a file offline, and dcision check-state checks 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.

On this page