Search Modaic documentation

Find a Modaic guide or API reference

Decisions with the SDK

A decision sends JSON state to a model and asks one or more typed questions. Each question ID becomes a key in the response's answers object.

Create a decision

import { Modaic } from "modaic";

const modaic = new Modaic();

const result = await modaic.decisions.create({
  state: {
    ticket: "I was charged twice for order 4832. Please refund one charge.",
  },
  model: "acme/support-priority",
  questions: {
    needs_refund: {
      type: "noul",
      instructions: "Should this customer receive a refund?",
      criteria: {
        true: "A duplicate or invalid charge should be refunded.",
        false: "The charge is valid or more information is required.",
      },
    },
    priority: {
      type: "choice",
      instructions: "Choose the support priority.",
      criteria: {
        low: "No financial or time-sensitive impact.",
        normal: "Routine customer issue.",
        high: "Financial impact or an urgent blocker.",
      },
    },
  },
  idempotencyKey: "ticket-4832-v1",
});

const priority = result.answers.priority;
if (priority?.type === "choice") {
  console.log(priority.choice, priority.confidence);
}
console.log(result.usage.inputTokens);

Use AsyncModaic and await modaic.decisions.create(...) for asynchronous Python. The arguments and response models are otherwise identical. The acme/support-priority example uses a saved model, not ZDR direct inference. For approved ZDR base-model calls, an idempotencyKey or idempotency_key is ignored and each call is a new operation.

Run a model directly

Models returned by models.create, models.get, or models.update also expose model.decisions.create. It accepts the same decision options except model, which is supplied automatically.

const model = await modaic.models.get("acme", "support-priority");
const result = await model.decisions.create({
  state: { ticket: "I was charged twice. Please refund one charge." },
});

With AsyncModaic, await both the model lookup and model.decisions.create(...). The model uses its originating client, so keep that client open while running decisions. This shorthand does not pin a revision; pass revision when needed. Model serialization still contains only response data.

Question types

Question typeUse it forMain result field
noulBoolean likelihoodanswer.noul
choiceOne option from a finite setanswer.choice
scoreA position on an ordered rubricanswer.score

Repository models load their saved questions automatically. Questions sent in a request override saved questions with matching IDs. Set capture to false to skip recording a repository-model call, or pass an example_id / exampleId to group it under a caller-assigned UUID.

Typed responses

Both SDKs can validate a decision into a type you declare.

Python

Pass a Pydantic model as response_model; it changes local parsing only and is never sent to the API.

Extending DecisionResponse is the shorter path. It already declares model, answers, and usage, and it lifts the answers you name to the top level:

from modaic import DecisionResponse, Modaic, NoulAnswer, ScoreAnswer


class ListingReview(DecisionResponse):
    photo_matches: NoulAnswer
    condition: ScoreAnswer


result = modaic.decisions.create(
    model="mo-fast",
    state={"listing": "Vintage leather jacket, size M"},
    questions={
        "photo_matches": {"type": "noul", "instructions": "Does the photo match?"},
        "condition": {"type": "score", "criteria": ["Worn", "Good", "Like new"]},
    },
    response_model=ListingReview,
)

print(result.photo_matches.noul, result.condition.score)

Field names must match question IDs. Use Field(alias="question-id") for IDs that are not Python identifiers, and avoid reusing response metadata names such as model or usage.

response_model also accepts a plain Pydantic BaseModel, for callers who want to own the whole schema. Declare the parts you care about, nested answers included:

from pydantic import BaseModel

from modaic import NoulAnswer


class ListingAnswers(BaseModel):
    photo_matches: NoulAnswer


class ListingEnvelope(BaseModel):
    answers: ListingAnswers


result = modaic.decisions.create(
    model="mo-fast",
    state={"listing": "Vintage leather jacket, size M"},
    questions={
        "photo_matches": {"type": "noul", "instructions": "Does the photo match?"}
    },
    response_model=ListingEnvelope,
)

print(result.answers.photo_matches.noul)

Pydantic drops response fields the model does not declare, so this validates answers.photo_matches and ignores the rest. Nothing is added to a plain model: request_id and the typed nouls / choices / scores views exist only on DecisionResponse subclasses. BaseModel itself is rejected, since Pydantic cannot validate into it.

Either way, a missing or mistyped answer that your model declares raises ModaicConnectionError.

TypeScript

The TypeScript SDK takes a schema instead. answers is a record of a union, so reading one answer normally means a key lookup and a narrowing check; a schema returns the exact shape:

import { z } from "zod";

const ListingReview = z.object({
  answers: z.object({
    photo_matches: z.object({ type: z.literal("noul"), noul: z.number() }),
    condition: z.object({ type: z.literal("score"), score: z.number() }),
  }),
});

const result = await modaic.decisions.create({
  model: "mo-fast",
  state: { listing: "Vintage leather jacket, size M" },
  questions: {
    photo_matches: { type: "noul", instructions: "Does the photo match?" },
    condition: { type: "score", criteria: ["Worn", "Good", "Like new"] },
  },
  schema: ListingReview,
});

console.log(result.answers.photo_matches.noul);

The schema validates the camelCase response the SDK returns, so usage is { inputTokens, outputTokens }. Like response_model, schema affects local parsing only and is never sent to the API, and it works on model.decisions.create too.

Zod is not a dependency of the SDK. schema accepts any object with a parse(value: unknown) => T method, so Valibot, ArkType, and hand-written validators work the same way; the exported ResponseSchema<T> type describes the contract. A response that fails validation raises ModaicConnectionError with the validator's own error kept as cause.

Pin a revision

const result = await modaic.decisions.create({
  state: { ticket: "The app crashes when I open settings." },
  model: "acme/support-priority",
  revision: "main",
});

console.log(result.checkpoint, result.revision, result.exampleId);

See the Decision API reference for the complete wire contract.

Optional reasoning over HTTP

The Decision API accepts reasoning_effort for mo-fast: low (512), medium (1,024), high (2,048), or xhigh (no explicit reasoning-token cap). Omit it for no reasoning. Context and request timeouts still apply. See the HTTP example for the exact wire format; the SDK examples above do not require this field.

Was this page helpful?