Docs
Cookbook

Structured data extraction

Turn free-form text into validated JSON: describe the schema in the system message, parse defensively, and retry once with the error fed back.

Extraction is the workhorse use case for a compact model: take a messy human sentence — "remind Deniz to send the invoice by March 3, and I need to book flights sometime" — and return machine-readable JSON your application can act on. tase-8b was trained to produce well-formed arguments from natural language, which makes it a strong fit for this recipe.

The recipe in three rules

  1. Put the schema in the system message. Spell out the exact JSON shape, the type of every field, and how to represent missing values. Tell the model to reply with the JSON object and nothing else — no prose, no code fences.
  2. Set temperature to 0. Extraction is a deterministic task. Zero temperature gives you the most repeatable output for the same input.
  3. Validate, and retry with the error. Never trust generated JSON blindly. Parse it, check required fields, and on failure send the broken output plus the validation error back and ask for a corrected object.

The retry is a conversation, not a do-over

Resending the same prompt from scratch gambles on getting lucky. Appending the invalid output and the exact validation error turns the retry into a correction — the model sees precisely what was wrong and fixes that.

JavaScript

A complete module using the openai npm package. The schema extracts tasks with an optional due date and assignee from any message.

import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.TASE_API_KEY, // tase_sk_...
  baseURL: 'https://api.tase.app/v1',
});

const SYSTEM_PROMPT = [
  'You extract structured data from messages.',
  'Reply with a single JSON object and nothing else - no prose, no code fences.',
  'Schema:',
  '{',
  '  "tasks": [',
  '    { "title": string, "due_date": string | null, "assignee": string | null }',
  '  ]',
  '}',
  'Use ISO 8601 dates (YYYY-MM-DD) when a date is present, otherwise null.',
  'Use null for assignee when no person is named.',
].join('\n');

// Validate the raw model output. Throws with a precise message on failure.
function parseTasks(raw) {
  const data = JSON.parse(raw); // throws on invalid JSON
  if (!Array.isArray(data.tasks)) {
    throw new Error('missing "tasks" array');
  }
  for (const task of data.tasks) {
    if (typeof task.title !== 'string' || task.title.length === 0) {
      throw new Error('every task needs a non-empty string "title"');
    }
  }
  return data.tasks;
}

export async function extractTasks(text, { retries = 1 } = {}) {
  const messages = [
    { role: 'system', content: SYSTEM_PROMPT },
    { role: 'user', content: text },
  ];

  for (let attempt = 0; attempt <= retries; attempt++) {
    const completion = await client.chat.completions.create({
      model: 'tase-8b',
      messages,
      temperature: 0,
    });

    const raw = completion.choices[0].message.content ?? '';
    try {
      return parseTasks(raw);
    } catch (err) {
      if (attempt === retries) throw err;
      // Feed the failure back and ask for a corrected object.
      messages.push({ role: 'assistant', content: raw });
      messages.push({
        role: 'user',
        content:
          'That was not valid: ' +
          err.message +
          '. Reply again with only the corrected JSON object.',
      });
    }
  }
}

const tasks = await extractTasks(
  'Remind Deniz to send the invoice by March 3, and I need to book flights sometime.'
);
console.log(tasks);

Python

The same recipe with the openai package from PyPI. Note that json.loads failures and validation failures are handled by the same retry path.

import json
import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["TASE_API_KEY"],  # tase_sk_...
    base_url="https://api.tase.app/v1",
)

SYSTEM_PROMPT = """You extract structured data from messages.
Reply with a single JSON object and nothing else - no prose, no code fences.
Schema:
{
  "tasks": [
    { "title": string, "due_date": string | null, "assignee": string | null }
  ]
}
Use ISO 8601 dates (YYYY-MM-DD) when a date is present, otherwise null.
Use null for assignee when no person is named."""


def parse_tasks(raw: str) -> list[dict]:
    data = json.loads(raw)  # raises on invalid JSON
    tasks = data.get("tasks")
    if not isinstance(tasks, list):
        raise ValueError('missing "tasks" array')
    for task in tasks:
        if not isinstance(task.get("title"), str) or not task["title"]:
            raise ValueError('every task needs a non-empty string "title"')
    return tasks


def extract_tasks(text: str, retries: int = 1) -> list[dict]:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": text},
    ]

    last_error: Exception | None = None
    for attempt in range(retries + 1):
        completion = client.chat.completions.create(
            model="tase-8b",
            messages=messages,
            temperature=0,
        )
        raw = completion.choices[0].message.content or ""
        try:
            return parse_tasks(raw)
        except (ValueError, json.JSONDecodeError) as err:
            last_error = err
            if attempt == retries:
                break
            # Feed the failure back and ask for a corrected object.
            messages.append({"role": "assistant", "content": raw})
            messages.append(
                {
                    "role": "user",
                    "content": f"That was not valid: {err}. "
                    "Reply again with only the corrected JSON object.",
                }
            )
    raise last_error


tasks = extract_tasks(
    "Remind Deniz to send the invoice by March 3, and I need to book flights sometime."
)
print(tasks)

Expected output

For the example sentence, both versions return the same structure. The second task has no date and no person, so both fields come back as null instead of being invented:

{
  "tasks": [
    { "title": "Send the invoice", "due_date": "2026-03-03", "assignee": "Deniz" },
    { "title": "Book flights", "due_date": null, "assignee": null }
  ]
}

Design nulls into the schema

If the schema has no way to say "not present", the model will fill gaps with plausible-looking guesses. Explicit null rules in the system message are what keep missing data missing.
Should I strip markdown code fences before parsing?+

The system message forbids fences, and at temperature 0 the model follows it reliably. If you want belt-and-braces safety, trim the raw string and remove a leading and trailing fence line before JSON parsing — it never hurts.

How many retries should I allow?+

One is almost always enough. If output fails validation twice in a row, the schema description is unclear — fix the system message rather than adding retries.

Can I use a JSON Schema validator instead of hand-written checks?+

Yes, and for larger schemas you should. Feed the validator error text into the retry message the same way — the more specific the error, the better the correction.

Next: migrate an existing app

Already running on an OpenAI-compatible stack? Switching to Tase is a two-line change.

Read the migration guide