Documentation

Build LLM-powered agents
with production-ready TypeScript

DSPy for TypeScript. Working with LLMs is complex—they don't always do what you want. DSPy makes it easier to build amazing things with LLMs. Just define your inputs and outputs (signature) and an efficient prompt is auto-generated and used. Connect together various signatures to build complex systems and workflows using LLMs.

15+ LLM Providers
End-to-end Streaming
Auto Prompt Tuning

DSPy Concepts

DSPy (Demonstrate-Search-Predict) is a framework from Stanford that replaces hand-written prompts with signatures — typed declarations of inputs and outputs. Ax implements DSPy in TypeScript with full streaming, validation, and automatic optimization.

Instead of writing prompts, you declare what goes in and what comes out. Ax generates the prompt, validates the output, and retries on failure.

Your First Signature

import { ai, ax } from '@ax-llm/ax';

const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });

// Declare inputs and outputs
const classifier = ax(
  'reviewText:string -> sentiment:class "positive, negative, neutral"'
);

const result = await classifier.forward(llm, {
  reviewText: 'This product exceeded my expectations!',
});
console.log(result.sentiment); // "positive"

The string reviewText:string -> sentiment:class "positive, negative, neutral" is a signature. Everything before -> is an input, everything after is an output. Each field has a name and a type.

Field Types

TypeDescriptionExample
stringTextname:string
numberInteger or decimalscore:number
booleanTrue/falseisValid:boolean
dateDate (YYYY-MM-DD)publishedOn:date
datetimeDate and timecreatedAt:datetime
jsonStructured JSON objectmetadata:json
codeCode blocksolution:code
classEnum classificationmood:class "happy, sad, neutral"
urlURLsourceLink:url
imageImage (input only)photo:image
audioAudio (input only)recording:audio

Modifiers

// All modifiers in action
const gen = ax(`
  question:string ->
  reasoning!:string "step by step thinking",
  answer:string,
  confidence:number "0-1",
  sources?:url[],
  tags:string[] "relevant keywords"
`);

Three Ways to Define Signatures

1. String-based (quick)

import { ax } from '@ax-llm/ax';

const gen = ax('question:string -> answer:string, confidence:number');

2. Fluent builder (type-safe with constraints)

import { AxGen, f } from '@ax-llm/ax';

const sig = f()
  .input('document', f.string('Text to analyze').min(10).max(10000))
  .output('summary', f.string('Brief summary').min(50).max(500))
  .output(
    'sentiment',
    f.class(['positive', 'neutral', 'negative'], 'Overall tone')
  )
  .output('confidence', f.number('Score 0-1').min(0).max(1))
  .output('tags', f.string('Keywords').array())
  .build();

const gen = new AxGen(sig);

3. Hybrid (extend a string signature with fluent fields)

import { s, f, AxGen } from '@ax-llm/ax';

const sig = s('text:string -> summary:string')
  .appendOutputField('entities', f.object({
    name: f.string().min(1).max(100),
    type: f.class(['person', 'organization', 'location']),
    confidence: f.number().min(0).max(1),
  }).array());

const gen = new AxGen(sig);

Validation Constraints

The fluent builder supports schema validation on fields. When the LLM output fails validation, Ax auto-retries with the error fed back into the prompt.

const sig = f()
  .input('document', f.string().min(10).max(10000))
  .output('analysis', f.object({
    summary: f.string('Brief summary').min(50).max(500),
    entities: f.object({
      name: f.string().min(1).max(100),
      type: f.class(['person', 'organization', 'location']),
      confidence: f.number().min(0).max(1),
    }).array(),
    contact: f.object({
      email: f.string().email(),
      website: f.string().url().optional(),
      username: f.string().min(3).max(20).regex('^[a-z0-9_]+$', 'lowercase alphanumeric'),
    }),
    tags: f.string().min(2).max(30).array(),
    createdAt: f.datetime(),
  }))
  .build();

String constraints: .min(len), .max(len), .email(), .url(), .date(), .datetime(), .regex(pattern, description)

Number constraints: .min(val), .max(val)

Assertions

Assertions validate LLM output after generation. If an assertion fails, Ax retries with the error message.

Post-generation assertions

const gen = ax('question:string -> answer:string, confidence:number');

// Return true (pass), false (fail), or a string (fail with message)
gen.addAssert(({ answer }) => {
  if (answer.length < 10) {
    return `Answer too short: ${answer.length} chars (min 10)`;
  }
  return true;
});

// With a fallback message
gen.addAssert(
  ({ confidence }) => confidence > 0.7,
  'Confidence must be above 70%'
);

// Throw for immediate failure (no retry)
gen.addAssert(({ answer }) => {
  if (answer.includes('offensive')) {
    throw new Error('Content moderation failed');
  }
  return true;
});

Streaming assertions

Validate output as it streams in, before generation completes:

gen.addStreamingAssert('answer', (content, done) => {
  if (!done) return undefined; // Wait until field is complete
  return content.length >= 10 ? true : 'Answer too brief';
});

Chain of Thought

Use ! (internal) fields to add reasoning steps that are generated but excluded from the final output. This improves accuracy on complex tasks.

// Without chain of thought
const simple = ax('problem:string -> solution:string');

// With chain of thought — reasoning is generated but not returned
const cot = ax('problem:string -> reasoning!:string, solution:string');

The LLM is forced to produce reasoning first, then solution. The ! means reasoning won’t appear in the result object — only solution is returned.

Multi-Modal

Image and audio fields work as inputs. The LLM receives the media directly.

const describe = ax('photo:image, question?:string -> description:string, objects:string[]');

const result = await describe.forward(llm, {
  photo: { url: 'https://example.com/photo.jpg' },
  question: 'What animals are in this image?',
});

Optimization

Signatures + examples enable automatic prompt tuning. Provide examples of correct input/output pairs, and the optimizer finds better prompts and demonstrations.

const gen = ax('question:string -> answer:string');

// Provide examples
gen.setExamples([
  { question: 'What is 2+2?', answer: '4' },
  { question: 'Capital of France?', answer: 'Paris' },
]);

For automatic optimization with MiPRO, GEPA, or ACE, see the Optimization Guide.

Streaming

All signatures support streaming. Pass stream: true to get results as they generate:

const gen = ax('question:string -> answer:string');

const result = await gen.forward(
  llm,
  { question: 'Explain quantum computing' },
  { stream: true }
);

Streaming works with assertions and validation — Ax validates fields as they complete.

Next Steps