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.
| Model | Provider | API | actionId |
|---|---|---|---|
typesafe_jev | TypeSafe | decision/1 | 83d17bdc-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": []
}
| Field | Meaning |
|---|---|
name, tool.name | The model's name, <provider>_<family>. A Starlark program calls it as tools.typesafe_jev(...). |
enabled | False for a retired version. |
config.provider | The model's provider. |
config.model | The provider's model release this version calls, such as jev-1.13.0. |
config.api | The API this version implements, such as decision/1. |
config.retired | On a retired version only: the date it was retired. |
tool.inputSchema | The API's input schema at this version. |
tool.outputSchema | The API's output schema at this version, closed at every level. |
policies | None. 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 type | criteria | Rule |
|---|---|---|
choice | A map from option to its description. | Required, with at least one option. A description can be null. |
score | An ordered array of level descriptions, from level 0. | Required, with at least two levels, none null. |
noul | An 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:
| Field | Meaning |
|---|---|
model | The release that answered. It always equals the version's config.model. |
answers | One answer per question, keyed by question ID, with its question's type. |
usage | input_tokens and output_tokens. |
| Answer | Fields beyond type |
|---|---|
choice | choice, one of the options; probabilities, a probability for each option; confidence. |
score | score, 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. |
noul | noul, 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.
| Model | Version | config.model | config.api | Status |
|---|---|---|---|---|
typesafe_jev | 1 | jev-1.13.0 | decision/1 | Current |
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.
| Code | When | Details and hint |
|---|---|---|
action_input_invalid | The input fails the API's rules. Nothing is sent. | The path and the expected shape. |
model_not_served | This 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_callable | The version is retired. | The release, the date, and the latest version. |
model_credential_missing | The deployment has no API key for the provider. Nothing is sent. | Ask your Twisp operator to set the key. |
model_unauthorized | The provider refused the deployment's key. | details.request_id. Ask your Twisp operator to replace the key. |
model_request_invalid | The 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_unavailable | The 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_invalid | The 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:
| Kind | data |
|---|---|
model.request | provider, method, path, and body, the request body exactly as sent. Never the API key. |
model.response | status, 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.