Ax Signature Reference
This skill helps an LLM generate correct DSPy signature code using @ax-llm/ax. Use when the user asks about signatures, s(), f(), field types, string syntax, fluent builder API, validation constraints, or type-safe inputs/outputs.
Install
Install only this skill for TypeScript:
npx skills add https://ax-llm.github.io/ax/typescript/ --skill 'ax-signature'Published skill file: ax-signature/SKILL.md.
Source
- Source: src/ax/skills/ax-signature.md
- Version:
23.0.5
Skill Instructions
Signature Syntax
[description] input1:type, input2:type -> output1:type, output2:typeField Types
| Type | Syntax | TypeScript | Example |
|---|---|---|---|
| String | :string | string | userName:string |
| Number | :number | number | score:number |
| Boolean | :boolean | boolean | isValid:boolean |
| JSON | :json | any | metadata:json |
| Date | :date | Date | birthDate:date |
| DateTime | :datetime | Date | timestamp:datetime |
| DateRange | :dateRange | { start: Date; end: Date } | travelDates:dateRange |
| DateTimeRange | :datetimeRange | { start: Date; end: Date } | meetingWindow:datetimeRange |
| Image | :image | {mimeType, data} | photo:image (input only) |
| Audio | :audio | input: AxAudioInput; output: AxChatAudioOutput | recording:audio, speech:audio |
| File | :file | {mimeType, data} | document:file (input only) |
| URL | :url | string | website:url |
| Code | :code | string | pythonScript:code |
| Class | :class "a, b, c" | "a" | "b" | "c" | mood:class "happy, sad" |
Date, datetime, and range fields are AI-friendly but strict. They accept ISO-style values, trim minor whitespace/casing issues, and parse ranges as { "start": "...", "end": "..." }, [start, end], start/end, or natural delimiters like start to end; invalid values and reversed ranges should fail validation rather than being silently autocorrected.
Arrays, Optional, and Internal Fields
'tags:string[] -> processedTags:string[]' // arrays
'query:string, context?:string -> response:string' // optional with ?
'problem:string -> reasoning!:string, solution:string' // internal with !
Extended String Grammar (Modifier Bags + Nested Objects)
The string form is constraint-complete: everything the fluent API expresses (except Standard Schema fields) can be written in the string. A type takes an optional comma-separated, order-free modifier bag in parentheses, and objects declare structured fields inline.
`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"`| Modifier | Applies to | Effect |
|---|---|---|
min N / max N | string, number | String length bounds / numeric value bounds |
format email|uri|date|date-time | string | Format validation |
pattern "regex" ["desc"] | string | Regex validation with optional description |
cache | top-level input | Prefix-cache breakpoint |
item "desc" | arrays | Per-item description: tags:string(item "a tag")[] |
<language> | code | Language of the snippet: snippet:code(python) |
object{ field:type, opt?:type }nests recursively; append[]for an array of objects.- Optional goes on the name (
userAge?:number), never after the type. - The string API is strict: a modifier that does not apply to its type (e.g.
minon a boolean) is a parse error, where the fluent API silently ignores it. - Inside
object{ ... }, the!internal marker, media types,cache, anditemare rejected (they only apply at the top level). - In quoted values, backslashes are doubled — a regex
\dis writtenpattern "\\d+". AxSignature.toString()renders every construct back to this grammar losslessly, so a signature round-trips — this is what lets a whole flow serialize its node contracts into mermaid%%axdirectives (see the ax-flow skill).
Signature Gallery
Real-world contracts, one line each — every entry below parses with s() as written (# lines are captions, not part of the signature):
# 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 }[]
# Contact enrichment: optional format-validated outputs
bioText:string -> contactEmail?:string(format email), websiteUrl?:string(format uri), birthDate?:string(format date)
# RAG: cached corpus input plus per-item described citations
corpusText:string(cache), userQuestion:string -> answerText:string, citedChunks:string(item "verbatim quote")[]
# Code generation: language-tagged code outputs
taskBrief:string -> pythonScript:code(python), testCases:code(python), riskNotes?:string
# Chain of thought: internal reasoning stripped from the result
problemText:string -> reasoning!:string, solutionText:string
# 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 }[] }
# 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"
# 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 }[]
# Translation: optional locale input
sourceText:string, targetLocale?:string -> translatedText:string, glossaryHits: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[]
# Booking window: date range, bounded party size, and flexibility flag
requestText:string -> stayWindow:dateRange, partySize:number(min 1, max 12), flexibleDates:boolean
# Contract dates: date fields plus bounded notice period
contractText:string -> effectiveDate:date, expiryDate?:date, autoRenews:boolean, noticeDays?:number(min 0)
# Link audit: URL arrays and an optional primary URL
pageText:string -> referencedUrls:url[], primaryUrl?:url
# Config generation: JSON output plus per-item warnings
requirementsText:string -> serviceConfig:json, setupWarnings:string(item "one warning")[]
# 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"
# Pull request review: diff code, cached guide, structured comments, and verdict
diffText:code(diff), styleGuide?:string(cache) -> reviewComments:object{ filePath:string, lineNumber:number(min 1), commentText:string(max 300) }[], overallVerdict:class "approve, revise"
# Incident triage: severity class, optional service, and per-item runbook steps
alertLog:string -> incidentSeverity:class "sev1, sev2, sev3", suspectedService?:string, runbookSteps:string(item "one step")[]
# Product listing: image and file inputs with constrained listing outputs
productPhoto:image, priceSheet?:file -> listingTitle:string(max 80), bulletPoints:string(item "one selling point")[], priceUsd?:number(min 0)
# Study cards: nested object array with an optional difficulty tag
chapterText:string -> flashCards:object{ questionText:string, answerText:string, difficultyTag?:string }[]Four Ways to Create Signatures
1. String-Based (Recommended for simple cases)
import { ax, s } from '@ax-llm/ax';
const gen = ax('input:string -> output:string');
const sig = s('query:string -> response:string');2. Pure Fluent Builder API
import { f } from '@ax-llm/ax';
const sig = f()
.input('userMessage', f.string('User input'))
.input('contextData', f.string('Additional context').optional())
.input('tags', f.string('Keywords').array())
.output('responseText', f.string('AI response'))
.output('confidenceScore', f.number('Confidence 0-1'))
.output('debugInfo', f.string('Debug info').internal())
.build();3. Standard Schema (zod / valibot / arktype)
.input() and .output() accept any Standard Schema v1 compatible library — no wrapper, no adapter. Three shapes work everywhere:
import { z } from 'zod';
import { f } from '@ax-llm/ax';
// Shape A: per-field schema — name first, then the schema, then optional ax hints
const sig = f()
.input('contextData', z.string().describe('Background context'), { cache: true })
.input('userQuestion', z.string().describe('Question to answer'))
.output('reasoning', z.string().describe('Step-by-step thinking'), { internal: true })
.output('answer', z.string().describe('Final answer'))
.build();
// Shape B: whole-object schema — decomposed into fields in declaration order
const sig2 = f()
.description('Answer questions from retrieved context')
.input(
z.object({
contextData: z.string().describe('Background context'),
userQuestion: z.string().describe('Question to answer'),
}),
{ fields: { contextData: { cache: true } } } // companion options map
)
.output(
z.object({
reasoning: z.string().describe('Step-by-step thinking'),
answer: z.string().describe('Final answer'),
}),
{ fields: { reasoning: { internal: true } } }
)
.build();Validation constraints from zod flow into ax’s prompt validation:
// String constraints: .email(), .url(), .min(), .max(), .regex()
// Number constraints: .min(), .max()
// Arrays: z.array(z.string())
// Enums: z.enum([...]) — NOTE: enum maps to ax class type, output fields only
const sig3 = f()
.input(z.object({
emailAddress: z.string().email().describe('Contact email'),
username: z.string().min(3).max(20).describe('Handle'),
score: z.number().min(0).max(100).describe('Numeric score'),
}))
.output(z.object({
priority: z.enum(['low', 'medium', 'high']).describe('Priority'),
summary: z.string().describe('Result'),
}))
.build();Companion options (AxFieldOptions) carry ax-specific hints that schema libraries don’t represent:
| Option | Effect |
|---|---|
{ cache: true } | Mark input field as a prefix-cache breakpoint |
{ internal: true } | Mark output field as internal scratchpad (stripped from result) |
The same Standard Schema shapes work on fn() tools via .arg(), .returns(), and .returnsField() — argument types are inferred from the schema:
import { z } from 'zod';
import { fn } from '@ax-llm/ax';
// Whole-object zod on a tool — AI-SDK-style
const lookupProduct = fn('lookupProduct')
.description('Look up a product by name and return its current details')
.arg(
z.object({
productName: z.string().min(1).describe('Exact product name'),
includeSpecs: z.boolean().optional(),
})
)
.returns(
z.object({
price: z.number(),
inStock: z.boolean(),
rating: z.number().min(1).max(5),
})
)
.handler(async ({ productName, includeSpecs }) => ({
price: 79.99,
inStock: true,
rating: 4.3,
}))
.build();
// Per-argument form — mix with f.*() args, attach ax hints
const searchDocs = fn('searchDocs')
.description('Search indexed docs')
.arg('query', z.string().min(1), { cache: true })
.arg('limit', z.number().int().positive().optional())
.returnsField('results', z.array(z.string()))
.handler(async ({ query }) => [])
.build();4. Hybrid
import { s, f } from '@ax-llm/ax';
const sig = s('base:string -> result:string')
.appendInputField('extra', f.json('Metadata').optional())
.appendOutputField('score', f.number('Quality score'));Fluent API Reference
Type creators:
f.string(desc),f.number(desc),f.boolean(desc),f.json(desc)f.image(desc),f.audio(desc),f.file(desc),f.url(desc)f.email(desc),f.date(desc),f.datetime(desc),f.dateRange(desc),f.datetimeRange(desc)f.class(['a','b','c'], desc),f.code(desc)f.object({ field: f.string() }, desc)
Chainable modifiers (method chaining only, no nesting):
.optional()- make field optional.array()/.array('list description')- make field an array.internal()- output only, hidden from final output.cache()- input only, mark for prompt caching
// Correct: pure fluent chaining
f.string('description').optional().array()
f.string('context').cache().optional()
f.object({ field: f.string() }, 'item desc').array('list desc')
// Wrong: nested function calls (removed)
f.array(f.string('description')) // REMOVED
f.optional(f.string('description')) // REMOVED
f.internal(f.string('description')) // REMOVED
Validation Constraints
String Constraints
f.string('username').min(3).max(20)
f.string('email').email()
f.string('website').url()
f.string('birthDate').date()
f.string('timestamp').datetime()
f.string('pattern').regex('^[A-Z0-9]')Number Constraints
f.number('age').min(18).max(120)
f.number('score').min(0).max(100)Complete Validation Example
const sig = f()
.input('formData', f.string('Raw form data'))
.output('user', f.object({
username: f.string('Username').min(3).max(20),
email: f.string('Email').email(),
age: f.number('Age').min(18).max(120),
bio: f.string('Bio').max(500).optional(),
website: f.string('Website').url().optional(),
tags: f.string('Tag').min(2).max(30).array()
}, 'User profile'))
.build();Cached Input Fields
const sig = f()
.input('staticContext', f.string('Context').cache())
.input('userQuery', f.string('Dynamic query'))
.output('answer', f.string('Response'))
.build();Field Naming Rules
Good: userQuestion, customerEmail, analysisResult, confidenceScore
Bad: text, data, input, output, a, x, val (too generic), 1field (starts with number)
Media Type Restrictions
- Image and file fields are top-level input fields only.
- Audio fields can be top-level inputs or single top-level outputs.
- Audio output fields are scripted speech artifacts: the model returns plain text, then Ax synthesizes
AxChatAudioOutput. - Media fields cannot be nested in objects.
- Media arrays are supported for inputs only; output
audio[]is not supported.
Common Patterns
// Chain of Thought
'problem:string -> reasoning!:string, solution:string'
// Classification
'email:string -> priority:class "urgent, normal, low"'
// Multi-modal input
'imageData:image, question?:string -> description:string, objects:string[]'
// Scripted speech output
'question:string -> speech:audio, summary:string'
// Data Extraction
'invoiceText:string -> invoiceNumber:string, totalAmount:number, lineItems:json[]'
// Constrained string form (no fluent builder needed)
'reviewText:string(max 2000) -> rating:number(min 1, max 5), themes:string(item "a theme")[]'
// Nested object output in the string form
'profileText:string -> profile:object{ fullName:string, age?:number(min 0) }'
// With description
'"Answer TypeScript questions" question:string -> answer:string, confidence:number'Critical Rules
- The string form is constraint-complete: reach for modifier bags (
string(max 500),number(min 0, max 10),string(format email)) and inlineobject{ ... }before switching to fluent/zod just for constraints. Reserve fluent/Standard Schema for zod/valibot-backed fields. - The string API is strict — a modifier that does not apply to its type is a parse error (the fluent API silently ignores it).
- Use
f()fluent builder, NOT nestedf.array(f.string())– those are removed. - Field names must be descriptive (not generic like
text,data,input). - Image/file media types are input-only, top-level only; audio may also be a single top-level output.
.internal()/{ internal: true }is output-only (for chain-of-thought reasoning)..cache()/{ cache: true }is input-only (for prompt caching).- Validation errors trigger auto-retry with correction feedback.
f.email(),f.url(),f.date(),f.datetime()are shorthand forf.string().email()etc.;f.dateRange()andf.datetimeRange()return{ start: Date; end: Date }.z.enum()maps to ax’sclasstype — only valid on output fields.- For multimodal inputs (images, audio, files) and scripted audio outputs, use
f.image()/f.audio()/f.file()— zod has no equivalent.
Examples
Fetch these for full working code:
- Standard Schema (zod) — zod with f() and fn(), all three shapes
- Fluent Signature — native fluent f() API
- Structured Output — structured output with validation
- Debug Schema — JSON schema validation