Models

Ask a built-in model typed questions about a state and get typed answers that code can act on.

A model is a built-in tool that asks a provider's model typed questions about a state and returns typed answers: a choice among options with its probabilities, a level on a scale, or the probability of yes. Use one where a decision needs judgment over text or records, such as which team owns a ticket or whether a note reports an active loss, and the rest of the decision is code.

Twisp ships every model, and every tenant has them without creating them. The first is typesafe_jev: TypeSafe's Jev model, called through the TypeSafe API. Twisp holds the provider's API key, so you never supply or store one.

A model is an action of kind model, read like any action:

query Models {
  actions {
    actions(where: { native: true, kind: "model" }, first: 50) {
      nodes {
        actionId
        version
        definition {
          name
          enabled
          config
          tool {
            description
            inputSchema
            outputSchema
          }
        }
      }
    }
  }
}

typesafe_jev reads with config: {"provider": "typesafe", "model": "jev-1.13.0", "api": "decision/1"}: the provider, the provider's model release that this version calls, and the API the version implements. You cannot create, update, or delete an action of kind model. A model has no triggers, tools, skills, session, or policies, because it performs no database operation. It runs only as a call: from invokeAction, an agent's tools, a Starlark program's tools.typesafe_jev(...), or an MCP server. See Models.

Questions and answers

typesafe_jev implements the decision/1 API. A call sends a state and a map of questions, keyed by your own question IDs. The state is what the model judges: a string, an object, or an array, such as a ticket or a record read from the ledger.

Question typeAsksAnswer
choiceWhich of these named options fits.The choice, with probabilities for each option and a confidence.
scoreWhere on an ordered scale this falls.The score, a position on a scale of levels numbered from zero, such as 1.34, with its legend, probabilities, and confidence.
noulA yes-or-no question.noul, the probability of yes.
{
  "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 }
    },
    "severity": {
      "type": "score",
      "instructions": "How severe is the problem?",
      "criteria": ["Cosmetic.", "Degraded.", "Blocked."]
    },
    "urgent": { "type": "noul", "instructions": "Does the customer report an active loss?" }
  }
}

The output has a fixed shape: model, the release that answered, answers keyed by question ID, and usage, the tokens the call used. Because answers carry probabilities, code can set its own thresholds, such as acting on urgent only above 0.9 and asking a person below it.

Versions

Each version of a model calls one release of the provider's model. Version 1 of typesafe_jev calls jev-1.13.0. When TypeSafe ships a new release, Twisp adds a version for it.

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 output field it returns is still returned. A change that would break callers ships as a model with a new name, and nothing moves to it on its own.

A tool reference chooses how it follows new versions, as for any tool:

  • Unpinned, { name: "typesafe_jev" }, calls the latest version, resolved when each execution of the caller starts. Your input and code keep working as new versions arrive, but a new release can answer differently. A session can move to a new release between turns.
  • Pinned, { name: "typesafe_jev", version: 1 }, always calls jev-1.13.0. Pin where answers must not change without a change of your own.

Each call's execution records the version it ran.

Retirement

When a provider stops serving a release, Twisp retires its version. A retired version reads with enabled: false and config.retired, the date it was retired, and stays listed among the model's versions. A call to it fails with action_not_callable, and the hint names the release, the date, and the latest version to call instead. An MCP server leaves it out of its tool list. A definition can keep a pin to a retired version, and learns at call time that it is no longer callable; move the pin, or reference the model unpinned.

Fixing the questions

When an agent calls a model, the questions usually belong to the definition, not to the agent's model. A caller's tool reference can fix questions, so the agent supplies only state, and every call asks exactly the questions you wrote. Changing them is then an edit of the caller, recorded as a new version. Fixed input keeps working as unpinned versions move, because versions only add. See Tool references.

Deciding and acting together

A model only answers. To act on its answer atomically, wrap it in a Starlark tool: the program builds the state, reading records with graphql where it needs them, calls the model, and writes based on the answer. The model call and the writes it leads to commit together, or not at all. This program takes the state as its input and returns the routing decision; a version that acts would follow the call with the graphql writes the answer calls for.

QUESTIONS = {
    "owner": {
        "type": "choice",
        "instructions": "Which team owns this ticket?",
        "criteria": {"payments": "Payouts or transfers.", "access": "Login or keys."},
    },
}

def run(event):
    out = tools.typesafe_jev({"state": event["data"], "questions": QUESTIONS})
    answer = out["answers"]["owner"]
    return {"owner": answer["choice"], "confidence": answer["confidence"], "model": out["model"]}

The wrapping tool declares the model in its tools list, and its own inputSchema is the shape of the state you accept. Callers, including agents, see one tool with a narrow input and a typed output, and the questions stay fixed in the program. Routing every caller through one such tool also means a move to a new model is a change to that tool alone.

Errors

A model call is made once; the caller decides whether to call again. Every failure below is a call error, which a Starlark program can handle with check=False and an agent sees as a failed call:

  • action_input_invalid: the input does not match the API, checked before anything is sent.
  • model_not_served: this deployment does not offer the model.
  • action_not_callable: the version is retired.
  • model_credential_missing: the deployment has no API key for the provider.
  • model_unauthorized: the provider refused the deployment's key.
  • model_request_invalid: the provider rejected the request.
  • model_unavailable: the provider was rate limited, overloaded, or did not answer within 10 seconds.
  • action_output_invalid: the response did not match the questions, or came from another release than the version names.

The first three are about your call. The others are about the deployment or the provider; tell your Twisp operator when one persists.

The call's record

Each call's execution records its input and output, as every call does. Its transcript also records the exchange with the provider: a model.request event with the request exactly as sent, written before it is sent, and a model.response event with the status, the provider's request ID, the latency, and the body exactly as received, written before the answer is used. The provider's API key is never recorded. Both events are kept even when the call's transaction rolls back, so the record of what left Twisp always survives. A field the provider adds to its response is not in the output until a version names it, but it is in model.response.

A preview or replay of an execution that called a model calls the provider again: it uses tokens and can get a different answer. A call repeated with the same executionId returns the recorded execution and does not call the provider.

Your data

The state and the questions are sent to the provider on every call; they leave Twisp. Send only what the judgment needs, and build the state in a wrapping tool when the records it reads hold more than that. An MCP server marks a model, and every tool or agent that declares one, as open world, so hosts know the call sends data outside Twisp. See MCP Servers.

Every tenant can call the models its deployment offers. To restrict who can, use policies: a direct call needs INVOKE on the model, as for any tool.

Next steps

  • Follow Classifying with a Model to call one, wrap it in a tool, and read its record.
  • Read Tools for calls, nested calls, and check=False.
  • See the Models reference for the configuration, input, output, versions, errors, and transcript.
Previous
Skills