Convilyn developers

Consumer SDK

One call, one JSON object.

Hand understand() your files and the JSON Schema you want back. The platform returns a result in that shape, grounded against the inputs before it reaches you.

The call

import json
from convilyn import Convilyn
 
client = Convilyn()
file = client.files.upload("invoice-2026-03.pdf")
 
result = client.goals.understand(
    [file.file_id],
    schema=json.load(open("invoice.schema.json")),
    instructions="Amounts are in the invoice's own currency; do not convert.",
)
print(result["invoice_number"], result["total"])

TypeScript SDK is in pre-release — see its reference section.

Accepts documents (pdf, docx, doc, pptx, ppt, txt, md) and images (png, jpg, jpeg, webp), up to 50 MB each, as file ids from files.upload.

Returns the parsed JSON conforming to your schema. Not a job handle — the call drives the run to completion and hands you the object.

Why a JSON Schema, and not a model class

A Pydantic model would be more idiomatic in Python and useless everywhere else. JSON Schema is language-neutral, so the same invoice.schema.json drives the Python SDK, the CLI, and every other language binding — and the SDK adds no validation dependency to your install to support it.

It is also why extract() is deprecated. That call ran one fixed workflow and returned whatever shape it happened to emit; the caller had no say. Passing the shape in is the whole difference.

Design the schema for grounding, not for completeness

This is the part that decides whether you get reliable output, and the instinct to mark everything required is the wrong one.

Before — every field required:

{
  "type": "object",
  "required": ["invoice_number", "total", "discount_reason", "line_items"],
  "properties": {
    "invoice_number": { "type": "string" },
    "total": { "type": "number" },
    "discount_reason": { "type": "string" },
    "line_items": { "type": "array", "items": { "type": "object" } }
  }
}

An invoice with no discount now forces a choice between an empty value and an invented one. You have asked for a field the document cannot support.

After — required means "this document must contain it":

{
  "type": "object",
  "required": ["invoice_number", "total"],
  "properties": {
    "invoice_number": { "type": "string" },
    "total": { "type": "number" },
    "discount_reason": { "type": ["string", "null"] },
    "line_items": { "type": "array", "items": { "type": "object" } }
  }
}

Reliability here is the server's guarantee rather than something you tune per call — but only for shapes the source can actually support.

More best practice:

  • Ask for the narrowest schema that answers your question. Fields you do not read are fields that can be wrong.
  • Use instructions to steer, not to restate the schema. The shape is already fixed; the sentence is for the things a schema cannot say — which currency, which date convention, what to do with a struck-through line.
  • One file per call where you can. It is the case the platform handles most directly.
  • Check client.account.get_quota() before a batch, not after.

What can go wrong

RaisedMeansDo
UnderstandUnavailableErrorThis deployment does not accept schema-constrained requestsFall back to a workflow, or wait for enablement
ValueError / TypeErrorEmpty files, or schema is not a JSON Schema objectFix the call — raised before any network use
GoalJobFailedErrorThe run started and failedInspect the error; retry is not automatic
GoalJobTimeoutErrorThe deadline elapsedThe run survives — poll it, or raise timeout

The first row is the important one: an answer that was not grounded is never returned as though it were. A capability that is off fails loudly rather than degrading into a plausible guess.

From the shell

convilyn goals understand \
  --files file_abc \
  --schema-file invoice.schema.json \
  --instructions "Amounts are in the invoice's own currency" \
  --json

--dry-run validates the schema file and exits without a network call. A schema that is missing, unreadable, or not a JSON object fails before anything is sent — including under --dry-run, because a preview that skipped validation would be lying about what a real run does.

Where to go next