s() Signatures
Use s() when you want a parsed signature object. Use the string form directly in ax() or agent() when you do not need to inspect or compose the signature.
import { s } from '@ax-llm/ax';
const sig = s('email:string -> priority:class "high, normal, low"');Signatures are the contract shared by generation, tools, examples, validation, and optimizer traces.
What It Does
s() parses the Ax signature grammar into a signature object. That object can be reused, inspected, extended, passed into ax(), passed into agent(), or combined with fluent/schema fields where the language surface supports it.
flowchart LR A["Signature string"] --> B["Parsed signature object"] B --> C["Input fields + types"] B --> D["Output fields + class options"] C --> E["Prompt contract"] D --> E D --> F["JSON schema"] D --> G["Parser + validators"] B --> H["Reuse in ax() or agent()"]
Core Call Shape
signature = s("input:type -> output:type")
program = ax(signature)Common Patterns
- Keep string signatures for simple contracts.
- Use parsed signatures when several programs share the same input/output shape.
- Use fluent/schema builders when Standard Schema (zod/valibot) fields matter; everything else — constraints, nested objects, caching — is expressible in the string form.
- Use
classfields for bounded labels instead of vague prose instructions. - Use optional fields only when missing data is truly acceptable.
- Use internal outputs for model scratch structure that should not reach callers.
Constraints and nested objects fit in the string form directly:
s('userAge:number(min 0, max 120), contextText:string(cache) -> profileList:object{ fullName:string, userAge?:number }[] "matched profiles"')More contracts in the same grammar — moderation, extraction, retrieval:
postText:string -> moderationVerdict:class "allow, review, block", flaggedSpans:object{ spanText:string, reasonNote:string }[]
invoiceText:string -> invoiceNumber:string(pattern "^INV-\\d+$" "INV- then digits"), totalAmount:number(min 0), lineItems:object{ description:string, quantity:number(min 1), unitPrice:number }[]
corpusText:string(cache), userQuestion:string -> answerText:string, citedChunks:string(item "verbatim quote")[]
schemaText:string(cache), questionText:string -> sqlQuery:code(sql), queryNotes?:string(max 200)
emailText:string -> eventTitle:string, startsAt:datetime, endsAt?:datetime, attendeeNames:string[]
claimText:string, policyText:string(cache) -> isCovered:boolean, confidenceScore:number(min 0, max 1), citedClause?:stringThe string API is strict: a modifier that does not apply to its type (say min on a boolean) is a parse error, where the fluent API would silently ignore it. AxSignature.toString() renders every construct back to this grammar losslessly, which is what lets flows serialize their node contracts into mermaid %%ax directives.
Flows as mermaid diagrams
Because every signature renders back losslessly, a whole flow can be written as — or exported to — a mermaid flowchart, with each node’s contract in a %%ax directive:
flowchart TD
%%ax classify: ticketText:string -> ticketClass:class "bug, billing"
%%ax reply: ticketText:string, ticketClass:string -> replyText:string
classify --> replyIn TypeScript, flow(diagram) compiles that string into a runnable flow and String(flow) renders any flow back, so flow(String(flow)) round-trips (toString({ direction: 'LR' }) for render options).
Parsed string
import { ax, s } from '@ax-llm/ax';
const sig = s(
'applicantNote:string, userAge:number(min 0, max 120), contactEmail:string(format email)' +
' -> decision:class "approve, review, reject", profileList:object{ fullName:string, userAge?:number }[] "matched profiles"'
);
const screen = ax(sig);Fluent or native schema surface
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();Validation 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();Hybrid composition
Start with the concise string grammar, then attach native fields when the language surface makes that clearer.
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
Treat signatures as API contracts. Renaming fields changes examples, traces, optimizer artifacts, and caller code. Prefer descriptive field names and validation over long prompt instructions.
See s() API.