Signatures
Signatures are the contract between your application and the model. They name the inputs, name the outputs, define field types, attach field descriptions, and give Ax enough structure to generate prompts, parse results, validate values, retry failures, expose tools, trace usage, and optimize examples.
from axllm import s
sig = s('email:string -> priority:class "high, normal, low"')flowchart LR A[Signature string or builder] --> B[Prompt contract] A --> C[JSON schema] A --> D[Parser] D --> E[Validation] E -->|valid| F[Typed output] E -->|invalid| G[Correction feedback] G --> B
Grammar
The compact form is:
inputField:type, optionalField?:type -> outputField:type, listField:type[]Everything before -> is input. Everything after -> is output. Field descriptions can be attached as quoted text, and narrow class fields make enum-style output explicit.
Types accept an optional modifier bag in parentheses, and objects can declare structured fields inline — the string form expresses everything the fluent API can (except Standard Schema fields):
userAge:number(min 0, max 120),
contactEmail:string(format email, cache),
codeSnippet:code(python)
->
userName:string(pattern "^[a-z_]+$" "lowercase name"),
tagList:string(item "a short tag")[] "all tags",
profileList:object{ fullName:string, userAge?:number(min 0) }[] "matched profiles"Modifiers that don’t apply to a type are hard errors — the string API is the
strict surface, whereas the fluent API silently ignores a .min() on a
boolean.
program = ax("question:string -> answer:string")
output = program.forward(client, {"question": "What is Ax?"})Per-value descriptions
Boolean fields can describe their outcomes with
urgent:boolean(true "Core task blocked", false "Routine request") "Urgent?".
Class fields can describe labels after their allowed-value list:
team:class "support, billing"(support "Usage help", billing "Invoice question").
Use .describeValues({ true: "Core task blocked", false: "Routine request" })
or .describeValues({ support: "Usage help", billing: "Invoice question" })
with the corresponding fluent fields.
These descriptions preserve the existing output types. Conventional providers
receive combined text descriptions in prompts and JSON schemas. Typesafe maps
them to native criteria. Descriptions survive signature round-tripping, and
rendering does not modify the original signature. These semantics also apply in
Python, Java, C++, Go, and Rust. Fluent field helpers use the language’s usual
describe_values, describeValues, or DescribeValues naming.
Per-field value guidance belongs in the signature. Scalar boolean conversion uses the provider threshold; evidence fields can specify their own local threshold.
Decision evidence and reranking
Use signature fields to access decision metadata with Jev, OpenAI Decisions, or a generative provider:
query:string, document:string ->
relevant:boolean(evidence, threshold 0.7),
category:class "support, billing"(evidence),
relevance:score "Irrelevant, Related, Partial answer, Direct answer"(evidence)Boolean evidence contains value, probability, threshold, and provenance.
Class evidence contains value, a complete label-to-probability map, and
provenance. Score evidence contains a continuous value from zero to the last
rubric index, ordered levels, ordered probabilities, and provenance.
Native class and score results can also contain the provider’s confidence.
A score without (evidence) returns only its numeric value. Existing scalar
boolean and class fields keep their existing behavior.
Ax preserves a native provider’s selected class, continuous score, probabilities, and confidence. A native value can differ from the probability argmax or weighted mean. With generative models, Ax requests probabilities, derives the boolean or class locally, and calculates a score as the probability-weighted rubric index. It preserves the supplied probabilities and never invents confidence.
provenance.source distinguishes native from elicited; provider and, when
available, model describe the actual response. These fields come from Ax, not
generated output. The common result shape does not make probabilities or
confidence equally calibrated across providers or models. Evaluate thresholds
and ranking quality on your own data. A score’s meaning depends on its declared
rubric; it is not a probability of correctness.
For reranking, evaluate each query/document pair, retain the candidate ID in your
application, and sort by relevance.value within the same rubric and model.
Ordinary Ax processors, assertions, result selection, traces, and caches receive
the decoded result. Streaming waits for complete evidence before emitting it.
import json
import os
from axllm import ai, ax
model = ai("openai-decisions", api_key=os.getenv("OPENAI_API_KEY") or os.environ["OPENAI_APIKEY"])
judge = ax('query:string, document:string -> relevant:boolean(evidence, threshold 0.7), relevance:score "Irrelevant, Related, Partial answer, Direct answer"(evidence)')
query = "How do I request a refund?"
candidates = [{'id': 'refund', 'text': 'Request a refund within 30 days through your account billing page.'}, {'id': 'password', 'text': 'Reset your password using the sign-in page.'}]
ranked = [{"id": item["id"], **judge.forward(model, {"query": query, "document": item["text"]})} for item in candidates]
ranked.sort(key=lambda item: item["relevance"]["value"], reverse=True)
print(json.dumps(ranked, indent=2))Use boolean(evidence, refusal return) (or an evidence field with refusal return)
to receive {status: "answered", ...evidence} or {status: "refused", provenance}.
By default, a refusal raises AxDecisionRefusalError and stops correction retries.
Generative providers support nested, optional, and array evidence fields. Native Jev and OpenAI Decisions signatures require flat, required scalar fields; Ax rejects unsupported shapes before transport. For advanced structured criteria or native request features, the direct provider client remains available. Label-only examples are accepted without manufactured distributions. Examples containing actual evidence preserve it while omitting runtime-owned result metadata from the inference demonstration.
Field Types
| Type | Example | Notes |
|---|---|---|
string | question:string | General text |
number | score:number | Parsed numeric value |
score | relevance:score "Low, Medium, High" | Continuous index on a declared rubric |
boolean | approved:boolean | True/false |
json | metadata:json | Structured object |
date / datetime | dueDate:date | Strict date-like values |
dateRange / datetimeRange | window:dateRange | { start, end } range values |
url / code | site:url, script:code | Specialized strings |
class | priority:class "high, normal, low" | Output class with known choices |
image / audio / file | photo:image | Media fields where provider/language supports them |
type[] | tags:string[] | Arrays |
object{ ... } | profile:object{ name:string, age?:number } | Nested structured fields, recursively; also object{ ... }[] |
type(modifiers) | score:number(min 0, max 10) | Constraint/metadata bag, see below |
Modifier bag entries (comma-separated, order-free):
| Modifier | Applies to | Effect |
|---|---|---|
min N / max N | string, number | Length constraints on strings, value bounds on numbers |
format email|uri|date|date-time | string | Format validation |
pattern "regex" ["description"] | string | Regex validation with an optional human-readable description |
cache | top-level inputs | Marks the field for provider context caching |
item "description" | arrays | Per-item description, e.g. tags:string(item "a short tag")[] |
<language> | code | Programming language, e.g. snippet:code(python) |
Good signatures use domain names: customerEmail, policyQuestion, riskScore. Avoid input, data, output, and other names that force the model to infer intent from prose.
Fluent API
The fluent builder is the best option when a signature needs constraints, descriptions, cache hints, internal fields, or programmatic composition. In TypeScript this is the f() builder. Generated language packages expose the AxIR-supported signature surface available in that package.
signature = (
f()
.input("requestText", f.string("Booking request").min(10).max(500))
.input("contactEmail", f.string("Contact email").email())
.output("partySize", f.number("Guests").min(1).max(12))
.build()
)Chainable field modifiers are deliberately simple:
| Modifier | Use |
|---|---|
.optional() / ? | Field can be absent |
.array() / [] | Field is a list |
.internal() / ! | Output scratch field, stripped from final output |
.cache() | Stable input prefix suitable for provider caching |
Validation And Retries
A signature is not just documentation. Ax validates parsed field values and can retry with correction feedback when the model returns malformed JSON, an invalid class value, a bad email, a reversed date range, or a value that violates schema constraints.
signature = (
f()
.output("partySize", f.number("Guests").min(1).max(12))
.output("bookingCode", f.string().regex(r"^[A-Z]{3}-\d{4}$"))
.build()
)Validation happens after parsing complete fields. For streaming generation, streaming assertions can fail fast at field boundaries and feed correction feedback into the next attempt.
Standard Schema
Standard Schema support is a TypeScript surface today. Use it when you want zod, valibot, or arktype constraints to sit beside the Ax signature and feed the validation/retry loop.
Generated language packages expose the AxIR-supported signature and JSON-schema surfaces for their native package. They preserve the same contract idea, but they do not pretend that TypeScript schema libraries exist in the target language.
Media, Cache, And Internal Fields
Media fields let a program receive images, audio, or files when the provider supports them. In agent flows, audio inputs are usually transcribed before planner/executor/responder stages; direct ax(...) programs can pass native media to compatible providers.
An image input is { mimeType, data }, a file input carries mimeType and either data or fileUri, and a url input is a string or { url, title, description }. Each also carries the optional keys its chat part declares to the provider or router: an image’s details (high, low, or auto; OpenAI’s image detail), cache, optimize, and altText; a file’s filename (OpenAI rejects inline file data without one), cache, and extractedText; a url’s cachedContent and cache; and an audio input’s mimeType, sampleRate, channels, cache, transcription, and duration. Other keys stay behind. OpenAI and Anthropic-style providers read a url input as text: its cached content, or else its title, description, and url, each on its own line.
An audio output field asks the model for text to speak. With the render_audio (or renderAudio) forward or constructor option, the generator turns that text into audio with the AI client’s speak(), as TypeScript does: the field becomes the speech result (data, format, mimeType) with the text as its transcript. Without the option the field keeps the text, and a one-time deprecation warning names the option; speaking becomes the default in the next major version.
An audio input given as a string, or as an audio object with a string transcript (such as a rendered audio output passed to the next program), reaches the model as text. Audio without a transcript goes as an audio part.
Cache hints mark stable context so provider prefix caching can reuse expensive prompt regions. Internal fields let a program ask the model to produce private scratch structure without exposing it in the final typed output.
Reuse And Composition
Start with a string signature, then graduate to parsed or fluent signatures when you need reuse.
signature = f().input("requestText", f.string().min(10)).output("partySize", f.number().min(1)).build()
program = ax(signature)
output = program.forward(client, {"requestText": request, "contactEmail": email})Signatures As Flow Diagrams
Because the string grammar is text-complete, a whole workflow of signatures can live in one mermaid flowchart — each node’s contract travels in a %%ax comment directive:
flowchart TD
%%ax summarize: documentText:string -> summaryText:string(max 500)
%%ax format: summaryText:string -> finalReport:string
summarize --> formatIn TypeScript, passing the diagram to flow() compiles it into a runnable flow, and String(flow) renders any flow back — so the diagram and the program stay in sync.
Signature Gallery
Real-world contracts, one line each — every entry parses as written:
# Support triage: several class outputs plus a capped reply draft
ticketText:string -> priorityClass:class "p0, p1, p2", sentimentClass:class "angry, neutral, happy", replyDraft:string(max 500)
# Invoice extraction: regex-validated id, bounded totals, structured line items
invoiceText:string -> invoiceNumber:string(pattern "^INV-\\d+$" "INV- then digits"), totalAmount:number(min 0), lineItems:object{ description:string, quantity:number(min 1), unitPrice:number }[]
# RAG: cached corpus input plus per-item described citations
corpusText:string(cache), userQuestion:string -> answerText:string, citedChunks:string(item "verbatim quote")[]
# Resume parsing: nested objects inside nested arrays
resumeText:string -> candidateProfile:object{ fullName:string, yearsExperience:number(min 0), skillList:string[], education:object{ schoolName:string, degreeName?:string }[] }
# Code generation: language-tagged code outputs
taskBrief:string -> pythonScript:code(python), testCases:code(python), riskNotes?:string
# Lead scoring: signature-level description, bounded score, class next step
"Score sales leads" leadNotes:string -> fitScore:number(min 0, max 100) "0-100 fit", nextStep:class "call, email, drop"
# Contact enrichment: optional format-validated outputs
bioText:string -> contactEmail?:string(format email), websiteUrl?:string(format uri), birthDate?:string(format date)
# Chain of thought: internal reasoning stripped from the result
problemText:string -> reasoning!:string, solutionText:string
# Multimodal: top-level image input with an optional question
productPhoto:image, question?:string -> productDescription:string, detectedObjects:string[]
# Meeting audio: audio input, capped summary, per-item action list
meetingAudio:audio -> meetingSummary:string(max 1000), actionItems:string(item "one action item")[]
# Moderation: class verdict plus structured flagged spans
postText:string -> moderationVerdict:class "allow, review, block", flaggedSpans:object{ spanText:string, reasonNote:string }[]
# Text-to-SQL: cached schema plus SQL-tagged output
schemaText:string(cache), questionText:string -> sqlQuery:code(sql), queryNotes?:string(max 200)
# Calendar extraction: datetime fields and an optional end
emailText:string -> eventTitle:string, startsAt:datetime, endsAt?:datetime, attendeeNames:string[]
# Claims gate: cached policy, bounded confidence, and optional citation
claimText:string, policyText:string(cache) -> isCovered:boolean, confidenceScore:number(min 0, max 1), citedClause?:string
# Earnings extraction: structured period data plus a class outlook
filingText:string(cache) -> revenueByPeriod:object{ periodLabel:string, amountUsd:number }[], guidanceTone:class "raise, hold, cut"Production Notes
- Keep fields small and typed. Split unrelated jobs into separate signatures or a flow.
- Use
classor schema validation for narrow domains instead of prose-only instructions. - Put stable context in cacheable inputs rather than repeating it in every field description.
- Use internal outputs for model scratch fields that should not become API response data.
- Treat generated JSON schema as a public contract when tools, SDKs, or external callers rely on it.
See Tools, s() signatures, and s() API.