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.
import { s } from '@ax-llm/ax';
const 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.
import { ax, s } from '@ax-llm/ax';
const sig = s('emailText:string -> priority:class "high, normal, low", rationale:string');
const classify = ax(sig);Field Types
| Type | Example | Notes |
|---|---|---|
string | question:string | General text |
number | score:number | Parsed numeric value |
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 |
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.
import { f } from '@ax-llm/ax';
const sig = f()
.description('Classify an inbound support email')
.input('emailText', f.string('Raw customer email').cache())
.input('accountTier', f.class(['free', 'pro', 'enterprise']).optional())
.output('priority', f.class(['high', 'normal', 'low']))
.output('reasoning', f.string('Private working notes').internal())
.output('reply', f.string('Customer-facing reply'))
.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.
import { f } from '@ax-llm/ax';
const sig = f()
.input('formText', f.string('Raw form submission'))
.output('email', f.string('Contact email').email())
.output('score', f.number('Risk score').min(0).max(100))
.output('tags', f.string('Tag').min(2).max(30).array())
.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
TypeScript accepts any Standard Schema v1 validator through the fluent builder. That includes zod, valibot, and arktype. You can attach schemas field-by-field, decompose a whole object schema into fields, and pass Ax-only companion options for cache and internal fields.
import { z } from 'zod';
import { f } from '@ax-llm/ax';
const sig = f()
.input(z.object({
context: z.string().describe('Reference context'),
question: z.string().min(1).describe('User question'),
}), { fields: { context: { cache: true } } })
.output(z.object({
reasoning: z.string().describe('Private reasoning'),
answer: z.string().min(1),
priority: z.enum(['low', 'normal', 'high']),
}), { fields: { reasoning: { internal: true } } })
.build();The same idea works for tools. fn().arg(), .returns(), and .returnsField() accept Standard Schema validators so the handler gets typed arguments and Ax gets validation feedback.
import { z } from 'zod';
import { fn } from '@ax-llm/ax';
const lookupProduct = fn('lookupProduct')
.description('Look up product inventory')
.arg(z.object({ productName: z.string().min(1), includeSpecs: z.boolean().optional() }))
.returns(z.object({ price: z.number(), inStock: z.boolean(), rating: z.number().min(1).max(5) }))
.handler(async ({ productName }) => ({ price: 79.99, inStock: true, rating: 4.3 }))
.build();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.
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.
import { f, s } from '@ax-llm/ax';
const sig = s('question:string -> answer:string')
.appendInputField('context', f.string('Stable reference context').cache().optional())
.appendOutputField('confidence', f.number('0..1 confidence').min(0).max(1));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.