Stop Writing Prompts by Hand! Ax Brings DSPy to TypeScript

B
Bright Coding
Author
Share:
Stop Writing Prompts by Hand! Ax Brings DSPy to TypeScript
Advertisement

Stop Writing Prompts by Hand! Ax Brings DSPy to TypeScript

What if I told you that every hour you spend tweaking prompts is an hour wasted? That the fragile string concatenation, the endless A/B testing of "You are a helpful assistant" variations, the brittle regex parsers extracting JSON from markdown code blocks—all of it could disappear?

Here's the painful truth most developers won't admit: prompt engineering is not a skill, it's a symptom. A symptom of frameworks that force you to speak the model's language instead of your own. You didn't learn TypeScript to become a prompt whisperer. You learned it to build type-safe, composable, maintainable software.

Yet here we are in 2024, with production AI apps held together by duct tape: handwritten prompts scattered across files, zero validation on outputs, and switching from GPT-4 to Claude requiring a complete rewrite. The Ax TypeScript DSPy framework exists to end this madness. Born from the same research-backed principles as Stanford's DSPy framework, Ax brings automatic prompt generation, runtime type safety, and a unified API across 15+ providers to JavaScript and TypeScript runtimes. No dependencies. No prompt templates. No parser nightmares.

Ready to see how the top AI engineering teams are building production systems without writing a single prompt by hand? Let's dive in.

What Is Ax? The TypeScript DSPy Framework Explained

Ax is a small, dependency-free TypeScript library that implements the DSPy (Declarative Self-improving Python) programming model for JS/TS environments. Created by Vikram Rangnekar (@dosco) with contributions from optimization researchers like @monotykamary, Ax represents a fundamental shift in how developers build AI applications.

The core insight behind DSPy—and by extension, Ax—is declarative over imperative. Instead of telling the model how to do something through elaborate prompts, you declare what you want through signatures: typed specifications of inputs and outputs. Ax compiles these signatures into optimized prompts at runtime, executes the call, parses the response, and returns fully typed values.

Here's why Ax is trending right now:

  • The prompt engineering bottleneck is real: Teams spend 30-40% of AI development time on prompt iteration. Ax eliminates this entirely.
  • Type safety in AI apps is non-negotiable: Runtime validation failures in production cost money and trust. Ax's streaming parser with retry pipelines catches issues before they reach users.
  • Multi-provider resilience: With rate limits, pricing changes, and model deprecations, vendor lock-in is dangerous. Ax's unified API lets you swap OpenAI for Anthropic, Google Gemini, or local Ollama instances with one parameter change.
  • The agent wave demands structure: 2024 is the year of AI agents, but most implementations are spaghetti code. Ax's AxAgent provides a principled three-stage pipeline with sandboxed execution.

Unlike Python's DSPy which requires a Python runtime, Ax runs everywhere TypeScript does: Node.js, Bun, Deno, and all modern browsers. It's production-tested and battle-hardened across real-world deployments.

Key Features That Make Ax Insanely Powerful

Ax packs an enormous amount of capability into a zero-dependency package. Here's what separates it from every other AI framework in the TypeScript ecosystem:

Signature-Driven Development: At the heart of Ax is the signature—a declarative contract between your code and the AI. You can express signatures three ways: a concise string DSL ('review:string -> sentiment:class "positive, negative, neutral"'), a fluent f() builder with nested objects and arrays, or any Standard Schema v1 validator (Zod, Valibot, ArkType). All three compile to the same optimized prompt pipeline.

Automatic Prompt Compilation: Ax doesn't just wrap API calls. It analyzes your signature, generates few-shot examples automatically, structures the prompt for the specific provider, and handles output parsing. The prompt you never write is the prompt you never have to debug.

Streaming Validation: Most frameworks stream raw text. Ax streams parsed, validated fields—the parser operates at field boundaries, so you get type-safe partial results as they arrive. This enables responsive UIs with guaranteed data integrity.

Multi-Modal Native: Images, audio, files—Ax treats them as first-class signature types. The f.image(), f.audio(), and f.file() helpers integrate seamlessly with OpenAI's vision models, Gemini's Live API, and Anthropic's multimodal capabilities.

Conversational Audio I/O: Beyond text, Ax supports real-time voice conversations through OpenAI's gpt-realtime-2 and gpt-realtime-whisper, plus Gemini's native audio. Generated audio streams as base64 PCM16 bytes for immediate playback.

Production Optimizers: GEPA (multi-objective Pareto optimization) and ACE (Automatic Curriculum Extraction) automatically improve your prompts based on real metrics. Define what "better" means—accuracy, brevity, cost, latency—and let the optimizer explore the prompt space.

Sandboxed Agent Runtime: The AxJSRuntime provides a hardened JavaScript execution environment for agent tools, with granular permissions (network, filesystem, storage, child process) and defense-in-depth security across all supported runtimes.

Real-World Use Cases Where Ax Dominates

1. Structured Data Extraction at Scale

E-commerce platforms, document processors, and CRM systems need to extract structured information from unstructured text. Traditional approaches use brittle regex or expensive fine-tuned models. With Ax, you declare the shape once and get typed outputs with automatic retry on validation failures:

const extract = ax(`
  customerEmail:string, currentDate:datetime ->
  priority:class "high, normal, low",
  sentiment:class "positive, negative, neutral",
  ticketNumber?:number,
  nextSteps:string[],
  estimatedResponseTime:string
`);

The ? marks optional fields, class constrains to enumerated values, and arrays/objects nest arbitrarily deep. Switch providers without touching this code.

2. Autonomous Research Agents

Legal tech, market research, and academic tools need agents that iteratively search, analyze, and synthesize information. Ax's AxAgent with recursive runtime (RLM) keeps long documents out of the root prompt:

  • Distiller: Breaks the user query into sub-tasks
  • Executor: Runs tool calls and sandboxed JS in a loop
  • Responder: Synthesizes final typed output

Checkpointed replay means older turns collapse into summaries instead of unbounded context growth. A 200-page contract analysis stays within token limits through intelligent context management.

3. Multi-Step Workflow Orchestration

Customer support automation, content pipelines, and data enrichment flows need sequential or parallel AI steps with type-safe handoffs. AxFlow provides a typed DAG where state types evolve as you add nodes:

flow<{ emailText: string }>()
  .n("classifier", 'emailText:string -> priority:class "high, normal, low"')
  .n("rationale", "emailText:string, priority:string -> rationale:string")
  .e("classifier", (s) => ({ emailText: s.emailText }))
  .e("rationale", (s) => ({ emailText: s.emailText, priority: s.classifierResult.priority }))

Each .n() adds a node, .e() wires execution, and the final .m() maps to output. The TypeScript compiler verifies that s.classifierResult.priority exists before you can use it.

4. Real-Time Voice Applications

Healthcare triage, accessibility tools, and hands-free interfaces need conversational audio. Ax unifies OpenAI's realtime models and Gemini's Live API under the same .chat() shape:

const stream = await voice.chat(
  { chatPrompt: [{ role: "user", content: "Say hello out loud." }] },
  { stream: true, webSocket: WebSocket },
);

Audio deltas stream as they're generated, enabling sub-second response latency for natural conversation flow.

Step-by-Step Installation & Setup Guide

Getting started with Ax takes under two minutes. The framework is intentionally zero-dependency for the core package, with optional add-ons for specific providers.

Basic Installation

# Core package — everything you need to start
npm install @ax-llm/ax

# Optional: AWS Bedrock provider for enterprise deployments
npm install @ax-llm/ax-ai-aws-bedrock

# Optional: Vercel AI SDK v5 integration for Next.js apps
npm install @ax-llm/ax-ai-sdk-provider

# Optional: MCP transports and JS runtime extras
npm install @ax-llm/ax-tools

Environment Configuration

Ax reads API keys from environment variables. Create a .env file or export directly:

# Required for your chosen provider
export OPENAI_APIKEY="sk-..."
export ANTHROPIC_APIKEY="sk-ant-..."
export GOOGLE_APIKEY="..."

# Optional: for multi-provider fallbacks
export MISTRAL_APIKEY="..."
export GROQ_APIKEY="..."

Provider Setup

Ax supports 15+ providers through a unified configuration. The ai() factory accepts a name and optional config:

import { ai } from "@ax-llm/ax";

// OpenAI (default)
const openai = ai({ name: "openai", apiKey: process.env.OPENAI_APIKEY });

// Anthropic Claude
const anthropic = ai({ name: "anthropic", apiKey: process.env.ANTHROPIC_APIKEY });

// Google Gemini
const gemini = ai({ name: "google-gemini", apiKey: process.env.GOOGLE_APIKEY });

// Local Ollama — no API key needed
const local = ai({ name: "ollama" });

// With model override
const gpt4o = ai({
  name: "openai",
  apiKey: process.env.OPENAI_APIKEY,
  config: { model: "gpt-4o" }
});

Running Examples

The repository includes 70+ runnable examples. Clone and execute:

git clone https://github.com/ax-llm/ax.git
cd ax
npm install

# Run any example with your API key
OPENAI_APIKEY=your-key npm run tsx ./src/examples/extract.ts
OPENAI_APIKEY=your-key npm run tsx ./src/examples/agent.ts
OPENAI_APIKEY=your-key npm run tsx ./src/examples/streaming1.ts

Key examples to explore: extract.ts, react.ts, agent.ts, streaming1.ts, multi-modal.ts, audio-chat.ts, standard-schema.ts, rlm-memories-and-skills.ts, gepa-flow.ts, ace-train-inference.ts.

Advertisement

REAL Code Examples from the Repository

Let's examine actual code from the Ax repository, with detailed explanations of how each pattern works in production.

Example 1: The 30-Second Quick Start

This is the simplest possible Ax program—yet it demonstrates the entire philosophy:

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

// Initialize any supported provider
const llm = ai({ name: "openai", apiKey: process.env.OPENAI_APIKEY });

// Declare a signature: input types -> output types with constraints
const classify = ax(
  'review:string -> sentiment:class "positive, negative, neutral"',
);

// Execute: Ax compiles the prompt, calls the API, parses and validates
const { sentiment } = await classify.forward(llm, {
  review: "This product is amazing!",
});
// sentiment: "positive" — typed as the literal union "positive" | "negative" | "neutral"

What's happening here? The string signature 'review:string -> sentiment:class "positive, negative, neutral"' declares: take a string input named review, produce an output named sentiment that must be one of three class values. Ax compiles this to a provider-specific prompt with automatic few-shot examples, handles the API call, parses the response, validates against the constraint, and retries if validation fails. The result is destructured with full TypeScript inference—sentiment is typed as the literal union, not string.

The critical line: name: "openai" can become "anthropic", "google-gemini", "mistral", "ollama", etc. Same signature, same code, different provider.

Example 2: Nested Objects with the Fluent Builder

Real-world data is deeply nested. The f() builder provides type-safe construction with arbitrary nesting:

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

// Build a complex signature through method chaining
const productExtractor = f()
  .input("productPage", f.string())  // Simple string input
  .output("product", f.object({      // Complex nested object output
    name: f.string(),
    price: f.number(),
    specs: f.object({                // Nested object
      dimensions: f.object({         // Double nesting
        width: f.number(),
        height: f.number()
      }),
      materials: f.array(f.string()), // Array of primitives
    }),
    reviews: f.array(f.object({      // Array of objects
      rating: f.number(),
      comment: f.string()
    })),
  }))
  .build();  // Compile to signature format

// Create the executable function
const gen = ax(productExtractor);

// Execute with full type inference through the chain
const { product } = await gen.forward(llm, { productPage: "..." });
// product.specs.dimensions.width is typed as number end-to-end

Why this matters: Without Ax, you'd hand-write a prompt like "Extract the product name, price, specs with dimensions width and height as numbers, materials as a list, and reviews with rating and comment..." then pray the model outputs valid JSON, then write a custom parser, then add retry logic. With Ax, the types are the specification. The compiler ensures product.specs.dimensions.width exists and is a number. Runtime validation enforces it.

Example 3: Standard Schema v1 with Zod Integration

Ax embraces the emerging Standard Schema v1 specification, making it compatible with the entire validation ecosystem. Here's per-field, whole-object, and tool integration:

import { z } from "zod";
import { ax, f, fn } from "@ax-llm/ax";

// (1) Per-field Zod — mix freely with f.* fields
// Zod constraints feed into Ax's retry pipeline
const reviewSentiment = ax(
  f()
    .input("productName", z.string().describe("Reviewed product"))  // .describe() becomes prompt context
    .input("reviewText", z.string().min(10))  // .min() enforces retry if too short
    .output("sentiment", z.enum(["positive", "neutral", "negative"]))
    .output("score", z.number().min(1).max(10))  // Range validation with auto-retry
    .output("keyPoints", z.array(z.string()))
    .build(),
);

// (2) Whole-object Zod — declare once, decomposed into ordered fields
// Perfect for existing Zod schemas in your codebase
const productSummary = ax(
  f()
    .input(z.object({ productName: z.string(), buyerProfile: z.string() }))
    .output(z.object({
      headline: z.string(),
      pros: z.array(z.string()),
      cons: z.array(z.string()),
      recommendation: z.enum(["buy", "wait", "skip"]),
    }))
    .build(),
);

// (3) Whole-object Zod on fn() — typed tool definition for agents
// The .handler() executes when the LLM chooses to call this tool
const lookupProduct = fn("lookupProduct")
  .description("Look up a product by name")  // LLM-visible description
  .arg(z.object({
    productName: z.string().min(1),           // Required, non-empty
    includeSpecs: z.boolean().optional()      // Optional flag
  }))
  .returns(z.object({
    price: z.number(),
    inStock: z.boolean(),
    rating: z.number().min(1).max(5)
  }))
  .handler(async ({ productName }) => ({
    // Your actual implementation — database call, API fetch, etc.
    price: 79.99,
    inStock: true,
    rating: 4.3
  }))
  .build();

The power of integration: .min(), .max(), .email(), .url(), .regex() all feed Ax's normal retry pipeline—if the model outputs invalid data, Ax automatically re-prompts with error context. .refine(), .transform(), and .superRefine() execute at parse time for complex cross-field validation. This isn't "Zod support" as an afterthought; it's deep integration where validation logic drives prompt behavior.

Example 4: Agent with Recursive Runtime and Memory

Production agents need more than tool calling—they need long-horizon reasoning with context management:

import { agent, AxJSRuntime } from "@ax-llm/ax";

const analyzer = agent(
  "context:string, query:string -> answer:string, evidence:string[]",
  {
    agentIdentity: {
      name: "documentAnalyzer",
      description: "Analyze long documents with iterative code + sub-queries",
    },
    contextFields: ["context"],  // Fields managed by checkpointed replay
    runtime: new AxJSRuntime(),  // Sandboxed JS execution
    maxTurns: 20,                // Prevent runaway loops
    maxRuntimeChars: 2_000,      // Limit generated code size
    contextPolicy: { preset: "checkpointed", budget: "balanced" },
    executorOptions: { model: "gpt-4o-mini" },  // Cheaper model for tool loops
  },
);

const result = await analyzer.forward(llm, {
  context: veryLongDocument,  // 100k+ tokens handled via RLM
  query: "What are the main arguments and supporting evidence?",
});

The RLM secret sauce: The recursive runtime keeps the root prompt small by executing JavaScript in a persistent sandboxed session. The agent can call llmQuery(...) for sub-questions, manipulate data structures, and use checkpointed replay so older conversation turns collapse into summaries. Without this, a 20-turn agent conversation would exceed context limits; with RLM, it stays bounded.

Advanced Usage & Best Practices

Optimize with GEPA for Production Quality: Don't settle for "it works." Define what "better" means numerically and let Ax explore:

const optimizer = new AxGEPA({
  studentAI: cheapModel,      // Fast, cheap model for trials
  teacherAI: strongModel,     // Stronger model for evaluation
  numTrials: 16,
  minibatch: true,            // Faster convergence on large datasets
  minibatchSize: 6,
});

const result = await optimizer.compile(
  emailFlow,
  trainSet,
  async ({ prediction, example }) => ({
    accuracy: prediction.priority === example.priority ? 1 : 0,
    brevity: (prediction.rationale?.length ?? 0) <= 60 ? 1 : 0.4,
  }),
  { auto: "medium", validationExamples: valSet },
);

The Pareto front lets you choose your accuracy/cost/latency tradeoff explicitly.

Secure Your Agent Runtime: Default AxJSRuntime is hardened, but audit your permissions:

const runtime = new AxJSRuntime({
  permissions: [AxJSRuntimePermission.NETWORK], // Grant only what's needed
});
// Blocked by default: import(), filesystem, child_process, storage

Use Streaming for Responsive UIs: .streamingForward() provides parsed partial results:

for await (const chunk of extract.streamingForward(llm, input)) {
  // chunk.fields partially populated as parser confirms boundaries
  updateUI(chunk.fields);
}

Cache Strategically: Mark expensive intermediate results with { cache: true } to avoid redundant LLM calls across similar inputs.

Comparison with Alternatives

Feature Ax LangChain JS Vercel AI SDK Direct API Calls
Automatic prompt generation ✅ Native ❌ Manual templates ❌ Manual templates ❌ Entirely manual
Runtime type validation ✅ Built-in ⚠️ Partial ⚠️ Partial ❌ None
Multi-provider unified API ✅ 15+ providers ✅ Multiple ⚠️ Limited ❌ Per-provider code
Streaming parsed fields ✅ Field-boundary ⚠️ Raw text ⚠️ Raw text ❌ Raw text
Agent with sandboxed runtime ✅ AxJSRuntime ❌ External tools ❌ Not built-in ❌ Build yourself
Automatic optimization (GEPA/ACE) ✅ Built-in ❌ Not available ❌ Not available ❌ Not available
Zero dependencies (core) ✅ Yes ❌ Heavy deps ❌ Moderate deps ✅ Yes
Standard Schema v1 support ✅ Zod/Valibot/ArkType ⚠️ Zod only ⚠️ Zod only ❌ N/A
Multimodal (image/audio/file) ✅ Native types ⚠️ Provider-specific ⚠️ Provider-specific ❌ Manual handling
Conversational audio I/O ✅ Unified .chat() ❌ Not available ❌ Not available ❌ Provider-specific

When to choose Ax: You want declarative, type-safe AI development without prompt engineering overhead. You're building agents, workflows, or extraction pipelines where correctness matters. You need to switch providers or run hybrid local/cloud setups.

When others might fit: You need maximum control over every token (direct APIs), you're deeply invested in a specific ecosystem with existing abstractions (LangChain for Python interop), or you're building simple chat UIs where type safety isn't critical.

FAQ: Common Developer Concerns

Does Ax add latency compared to direct API calls? Minimal. Prompt compilation happens once per signature, cached thereafter. The streaming parser adds negligible overhead (~1-2ms) while providing field-level validation that prevents expensive retry storms from malformed outputs.

Can I use my existing Zod schemas with Ax? Absolutely. Any Standard Schema v1 validator works without adapters. Drop your existing z.object() schemas directly into f().input() and f().output() calls. .describe(), .min(), .max(), and custom .refine() validators all function as expected.

How does Ax handle model switching in production? The ai() factory accepts any supported provider name. For resilience, initialize multiple providers and implement fallback logic at the application layer. Ax's unified response format means zero code changes when swapping.

Is Ax suitable for browser-based applications? Yes. The core package is dependency-free and works in all modern browsers. For audio applications, you'll need to provide a WebSocket implementation (like the ws polyfill). The sandboxed runtime uses Web Workers in browser environments.

What's the difference between GEPA and ACE optimization? GEPA (Generalized Expected Pareto Improvement) is a multi-objective optimizer exploring prompt variations against your metrics. ACE (Automatic Curriculum Extraction) uses playbook-based iterative refinement with structured curriculum learning. Use GEPA for exploration, ACE when you have domain expertise to encode.

How does RLM compare to other agent context management? RLM's checkpointed replay is unique. Most agents accumulate context linearly, eventually hitting limits. RLM collapses older turns into summaries automatically, maintaining a bounded context window for unbounded task horizons.

Can I contribute or get support? The project is actively maintained with Discord community support, Twitter updates, and GitHub issues. Apache 2.0 licensing permits commercial use.

Conclusion: The Future of TypeScript AI Development Is Declarative

We've reached an inflection point in AI engineering. The teams shipping fastest aren't those with the best prompt engineering skills—they're the ones who eliminated prompt engineering entirely. The Ax TypeScript DSPy framework represents this paradigm shift: types as contracts, signatures as specifications, and optimization as infrastructure.

What started as a port of Stanford's DSPy research has evolved into a production-hardened system handling structured extraction, autonomous agents, conversational audio, and multi-objective optimization—all through declarative TypeScript code that any developer can read and maintain.

The question isn't whether you can afford to adopt Ax. It's whether you can afford to keep hand-writing prompts while your competitors don't.

Ready to stop writing prompts and start building? Clone the ax-llm/ax repository, run the 70+ examples, and join the Discord community where the maintainers and power users collaborate. Your future self—the one maintaining that AI system six months from now—will thank you.

Advertisement

Comments (0)

No comments yet. Be the first to share your thoughts!

Leave a Comment

Apps & Tools Open Source

Apps & Tools Open Source

Bright Coding Prompt

Bright Coding Prompt

Categories

Advertisement
Advertisement
Advertisement