Search Modaic documentation

Find a Modaic guide or API reference

Quickstart

Make your first typed decision in a few minutes: create a key, ask a base model two questions about a support ticket, then save those questions as a model of your own so they can be captured, labelled, and aligned.

Coming from Typesafe?

Modaic is compatible with Typesafe's System One API. Update your inference request:

  • Change https://api.typesafe.ai/v1/systemone to https://modaic.dev/api/v1/systemone.
  • Use a Modaic API key in the Bearer authorization header.
  • Set model to mo or mo-fast, or create a model with your existing questions and use its workspace/slug path.

Keep your existing state, questions, and answer parsing. Modaic uses the same answers and usage format. The model path belongs in the request body, not the URL. If your client appends /v1/systemone itself, configure its base URL as https://modaic.dev/api. When you are ready, step 3 below saves your questions as a model so you can stop sending them on every request.

If your workspace has activated Zero Data Retention, note that direct base-model calls and saved-model calls have different retention terms. Saving these questions as a model does not extend ZDR to that model.

1. Create an API key

Create a key in the Modaic dashboard, then store it in an environment variable.

export MODAIC_API_KEY="mdc_..."

The key needs the write:repository scope to create decisions.

Install an SDK, or skip this step to use cURL:

npm install modaic

2. Ask typed questions

Send any JSON value as state and one or more typed questions. Each key in questions becomes a key in the response's answers object. This calls a base model directly, so the questions travel with the request; see supported base models for the full list.

import { Modaic } from "modaic";

const modaic = new Modaic();

const result = await modaic.decisions.create({
  model: "mo-fast",
  state: {
    ticket: "I was charged twice for order 4832. Please refund one charge.",
  },
  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: "quickstart-ticket-4832",
});

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

Modaic returns one typed answer per question, each with a probability, and reports token usage. The raw API response uses snake case. The Python SDK exposes those fields as typed Pydantic models, while the TypeScript SDK maps them to idiomatic camel case. An illustrative response:

{
  "model": "mo-fast",
  "answers": {
    "needs_refund": {
      "type": "noul",
      "noul": 0.97
    },
    "priority": {
      "type": "choice",
      "choice": "high",
      "probabilities": {
        "low": 0.01,
        "normal": 0.05,
        "high": 0.94
      },
      "confidence": 0.94
    }
  },
  "usage": {
    "input_tokens": 186,
    "output_tokens": 2
  }
}

3. Make it yours

Save the same questions as a model in your workspace. From then on you send only state: the questions live in the model, every decision is captured as an example you can label, and alignment rewrites those questions from your labels. Use your own workspace/slug in place of acme/support-priority.

const model = await modaic.models.create({
  workspace: "acme",
  slug: "support-priority",
  model: "mo-fast",
  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.",
      },
    },
  },
});

// No questions here: the model already has them.
const decision = await model.decisions.create({
  state: { ticket: "I was charged twice for order 4832. Please refund one charge." },
  idempotencyKey: "quickstart-ticket-4832-saved",
});

console.log(decision.answers.priority, decision.exampleId, decision.checkpoint);

The response now also carries example_id, decision_id, checkpoint, and revision, tying each answer to the version of the model that produced it.

You can still send questions with a saved model. They are merged over the model's own questions by id for that one request. That overrides any instructions alignment has written for a matching id, and answers to ids the model does not define cannot be labelled or aligned. Treat it as a way to experiment, not the way to call a saved model.

4. Align it to your judgement

Every decision your model makes is captured as an example. Label a few of them with the answer you wanted, then run an alignment: Modaic rewrites the question instructions from your labels and commits them as a new version of the same model.

Start with the decisions the model was least sure about — model.examples.list() returns each example with its latestDecision and confidence. Label a handful, not everything.

// 1. Label the example the decision in step 3 created.
await model.examples.annotate(decision.exampleId, {
  groundTruth: { needs_refund: true, priority: "high" },
  groundReasoning: "Duplicate charge on a single order: refund one and treat as financial impact.",
});

// 2. Align from the current branch head.
const current = await modaic.models.get("acme", "support-priority");
const head = current.commit?.commitSha;
if (!head) throw new Error("Model has no commits to align from.");

const alignment = await current.jobs.alignments.create({
  branch: "main",
  sourceCommitSha: head,
  idempotencyKey: "quickstart-align-1",
});

// 3. Wait for it to finish, then check what it did.
const finished = await modaic.alignments.wait(alignment.id, { progress: true });
console.log(finished.status, finished.resultCommitSha, finished.error);

Ground truth is keyed by question id: noul questions take a boolean, choice questions take an option key, and score questions take a zero-based index into the criteria array. The first annotation assigns a stable train or test split, which later updates preserve.

None of the requests above set a metric-call budget. The API sizes it to the validation split at launch, which is the recommended default; pass maxMetricCalls (max_metric_calls in Python) only to override it. Metric calls are billed as decisions. wait returns terminal failed and cancelled runs too — check status and error rather than assuming success, and note that a wait timeout stops waiting, not the job.

If the run beats the starting instructions it commits them, increments the model's checkpoint, and returns a resultCommitSha; if not, the model is unchanged. Either way your next model.decisions.create({ state }) uses the current version — no code change.

After an alignment, re-running the step 3 script as written would send the original questions and discard the optimized instructions. The API refuses that with 409 alignment_would_be_discarded. Omit questions when updating an aligned model.

Next steps

  • SDK installation — configure Python or TypeScript, choose sync or async execution, and handle errors.
  • SDK decisions — question types, repository models, revision pins, and response handling in both languages.
  • Decision API — all question types, capture controls, and response fields.
  • Ingest examples — add a dataset and optional ground truth.
  • Aligning your model — the full alignment workflow: collecting examples, feedback guidelines, branches, and commits.
Was this page helpful?