Skip to content

Structured outputs ​

A structured output turns a conversation into a record: a JSON Schema you define, filled in from what was said. It is how a call becomes a booking, a lead, a support ticket or a qualification score instead of a paragraph somebody has to read.

Defining one ​

GET    /api/v1/structured-outputs
POST   /api/v1/structured-outputs
GET    /api/v1/structured-outputs/templates
GET    /api/v1/structured-outputs/{structuredOutput}
PATCH  /api/v1/structured-outputs/{structuredOutput}
DELETE /api/v1/structured-outputs/{structuredOutput}
POST   /api/v1/structured-outputs/{structuredOutput}/publish
bash
curl -X POST "$CALLAGENT_API_URL/structured-outputs" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "appointment",
    "name": "Appointment request",
    "prompt": "Extract the appointment the caller asked for. Leave a field null if it was not stated; never guess a date.",
    "schema": {
      "type": "object",
      "properties": {
        "service":    { "type": "string", "description": "What they want done." },
        "preferred_date": { "type": "string", "format": "date" },
        "urgency":    { "type": "string", "enum": ["routine", "soon", "emergency"] },
        "callback_number": { "type": "string" }
      },
      "required": ["service"],
      "additionalProperties": false
    }
  }'
ts
const output = await fetch(`${process.env.CALLAGENT_API_URL}/structured-outputs`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    key: 'appointment',
    name: 'Appointment request',
    prompt: 'Extract the appointment the caller asked for. Leave a field null if it was not stated.',
    schema: {
      type: 'object',
      properties: {
        service: { type: 'string' },
        preferred_date: { type: 'string', format: 'date' },
        urgency: { type: 'string', enum: ['routine', 'soon', 'emergency'] },
      },
      required: ['service'],
      additionalProperties: false,
    },
  }),
}).then((response) => response.json());

The schema is validated structurally when you save it, so a malformed schema is rejected at definition time rather than discovered on a live call.

GET /structured-outputs/templates returns starting points for the common shapes.

Fields ​

FieldNotes
keyStable identifier, unique per organization. This is what you look for in the result.
nameHuman label.
schemaJSON Schema. Keep it flat and keep additionalProperties: false.
promptThe extraction instruction. Say explicitly what to do when a field was not mentioned.
targetWhat the extraction reads.
model{ provider, model } — the extraction can use a cheaper model than the conversation did.
enabled

Like assistants and tools, structured outputs are versioned: publish freezes the current definition, so changing a schema does not retroactively alter how last month's calls were parsed.

Attaching to an assistant ​

Either through the pivot the API exposes, or in the assistant's analysisPlan:

yaml
analysisPlan:
  summaryEnabled: true
  structuredOutputs:
    - name: appointment
      schema: { … }

Writing a schema that survives contact with speech ​

Four rules, learned the expensive way:

  1. Make almost everything optional. A caller who did not mention a date has not given you a date. A required field the model cannot fill is a field the model will invent.
  2. Use enums for anything categorical. urgency as a free string produces "pretty urgent", "ASAP", "not urgent I guess". As an enum it produces one of three values you can branch on.
  3. Say what "unknown" looks like in the prompt. "Leave a field null if it was not stated; never guess a date" is worth more than any schema constraint.
  4. Keep it flat. Nested objects extracted from speech are where accuracy goes to die. Two structured outputs beat one with a sub-object.

Reading the result ​

A call's extraction lands on the call record and appears in the structured-outputs tab of the dashboard, in GET /calls/{call} and in the analysis.completed event.

Extraction is not currently run by the control plane

Definitions, versioning, publication, the attachment to an assistant and the columns that store a result are all implemented. What is missing is the trigger: the post-call analysis pipeline that would perform the extraction is not dispatched after a call finishes.

So a structured output you define today will not be filled in by the control plane. It is populated only if the gateway supplies it in the finalisation payload. See Analytics & cost and Implementation status.

Next ​

Documented against the code. Anything not yet implemented is labelled, never described as working.