Models

Call a built-in model with a state and typed questions, and get typed answers back.

Sends a state and typed questions to a provider's model and returns its typed answers. A model is a built-in action of kind model: Twisp ships it, every tenant has it, and actions(where: { native: true, kind: "model" }) lists every one. Its name is reserved in every tenant. A write of kind model is rejected: no tenant creates, updates, or deletes one. A model has no triggers, tools, skills, session block, or policies: it performs no database operation. It runs only as a call, from invokeAction, an MCP server, an agent's tools, or a Starlark program's tools.<name>(...), and needs INVOKE on the model like any tool.

ModelProviderAPIactionId
typesafe_jevTypeSafedecision/183d17bdc-ece0-59e6-9af3-6dbb76ea351d

The model action

{
  "name": "typesafe_jev",
  "kind": "model",
  "enabled": true,
  "description": "Model typesafe_jev, release jev-1.13.0.",
  "config": { "provider": "typesafe", "model": "jev-1.13.0", "api": "decision/1" },
  "tool": {
    "name": "typesafe_jev",
    "description": "Ask Jev typed questions about a state. Returns calibrated answers: a choice with probabilities, a score on a scale, or the probability of yes.",
    "inputSchema": { "...": "decision/1 input" },
    "outputSchema": { "...": "decision/1 output" }
  },
  "policies": []
}
FieldMeaning
name, tool.nameThe model's name, <provider>_<family>. A Starlark program calls it as tools.typesafe_jev(...).
enabledFalse for a retired version.
config.providerThe model's provider.
config.modelThe provider's model release this version calls, such as jev-1.13.0.
config.apiThe API this version implements, such as decision/1.
config.retiredOn a retired version only: the date it was retired.
tool.inputSchemaThe API's input schema at this version.
tool.outputSchemaThe API's output schema at this version, closed at every level.
policiesNone. A model performs no database operation.

config is read only: it describes the version. An MCP server annotates a model as open world, openWorldHint: true, since each call sends its input outside Twisp. See Annotations.

decision/1

Input. state is required: a string, an object, an array, or null. questions is required: a map from a question ID, which is not empty, to a question, with at least one entry. No other field is accepted.

Question typecriteriaRule
choiceA map from option to its description.Required, with at least one option. A description can be null.
scoreAn ordered array of level descriptions, from level 0.Required, with at least two levels, none null.
noulAn object with true and false descriptions.Optional. Without a true or false description that is not null, instructions must not be empty.

instructions and each description is a string, an object, an array, or null, except a score level, which is never null. An input that breaks these rules fails with action_input_invalid, naming the path, before anything is sent. The provider enforces its own limits on the number of options and levels. The input limit of 1 MiB bounds the state and the questions.

{
  "state": { "subject": "Payouts failing", "body": "My payouts have failed for 3 days." },
  "questions": {
    "owner": {
      "type": "choice",
      "instructions": "Which team owns this ticket?",
      "criteria": { "payments": "Payouts or transfers.", "access": "Login or keys.", "other": null }
    },
    "urgent": { "type": "noul", "instructions": "Does the customer report an active loss?" }
  }
}

Output. The output has exactly these fields:

FieldMeaning
modelThe release that answered. It always equals the version's config.model.
answersOne answer per question, keyed by question ID, with its question's type.
usageinput_tokens and output_tokens.
AnswerFields beyond type
choicechoice, one of the options; probabilities, a probability for each option; confidence.
scorescore, a position on the scale, such as 1.34, rather than a whole level; legend and probabilities, keyed by level number from zero as a string; confidence.
noulnoul, the probability of yes.

A field the provider adds to its response is not in the output until a version names it. The call's transcript keeps the response as received.

A caller's reference can fix questions, so an agent supplies only state. Fixed input is a CEL expression, so the questions are written as a CEL map literal:

tools: [
  {
    name: "typesafe_jev"
    input: {
      questions: """
      {
        "personal": {"type": "noul", "instructions": "Is this a personal purchase?"}
      }
      """
    }
  }
]

Versions and retirement

Version N of a model is its Nth release: it fixes config.model, the schemas, and the descriptions.

ModelVersionconfig.modelconfig.apiStatus
typesafe_jev1jev-1.13.0decision/1Current

Within a model's name, a new version only adds: a new release, an optional input field, or an output field. An input an earlier version accepts is still accepted, and every field it returns is still returned. A change that breaks either ships under a new name, such as typesafe_jev2. A reference with a version calls that release, always; one without a version calls the latest, resolved when each execution of the caller starts, as for every tool. A new release can answer differently, so pin a version where answers must not move without a change of your own.

A version whose release the provider no longer serves is retired: it reads with enabled: false and config.retired, and is still listed. A call to it, from any caller, fails with action_not_callable, whose hint names the release, the retirement date, and the latest version. An MCP server leaves it out of tools/list. A definition can keep or add a pin to it. The latest version can be retired, and then a reference without a version fails the same way until a new release ships.

Errors

The call is made once, with a 10-second timeout, capped by the transaction's deadline; the caller decides whether to call again. Each failure below is a call error, which check=False catches and an agent shows to its model.

CodeWhenDetails and hint
action_input_invalidThe input fails the API's rules. Nothing is sent.The path and the expected shape.
model_not_servedThis deployment does not offer the model. A caller that declares it still runs; only its calls to the model fail.details.model. Ask your Twisp operator to enable it.
action_not_callableThe version is retired.The release, the date, and the latest version.
model_credential_missingThe deployment has no API key for the provider. Nothing is sent.Ask your Twisp operator to set the key.
model_unauthorizedThe provider refused the deployment's key.details.request_id. Ask your Twisp operator to replace the key.
model_request_invalidThe provider rejected the request, with a status in the 400s other than 401, 403, 408, and 429.details.provider, up to 2 KiB of the provider's message, and details.request_id. The input is the likely cause; a withdrawn release is the other.
model_unavailableThe provider was rate limited, overloaded, failed, or did not answer within 10 seconds.details.request_id and details.retry_after when the provider sent them. Call again later.
action_output_invalidThe answers do not match the questions, the response fails the output schema, or the response's model differs from config.model.For a different model, details.config_model and details.response_model.

The call transcript

A model call's execution records its input and output, as every call does. Its invocation's transcript records the exchange with the provider in two events:

Kinddata
model.requestprovider, method, path, and body, the request body exactly as sent. Never the API key.
model.responsestatus, request_id, the provider's request ID, latency_ms, and body, the body exactly as received. A request that got no response has status 0 and error.

A body that is not valid UTF-8 is recorded as body_base64 instead of body. The request is recorded before it is sent, and the response before it is used; if either cannot be recorded, nothing more is sent and the call fails and is retried. The events commit on their own, so they survive a call whose transaction rolls back, and invocation.transcriptLastSequence is the last of them.

A replay or a preview of an execution that called a model calls the provider again and can get a different answer. A call repeated with the same executionId returns the recorded execution without calling the provider.

Previous
Skills