Migration Strategies for Updating Ax Projects: From v13 Factory Functions to v15

Migrate Ax projects from v13 to v15 by replacing template-literal signatures with factory functions, constructor-based classes with factory functions like ai() and agent(), and nested fluent helpers with pure-fluent chains to ensure type safety and future compatibility.

Ax's evolution from v13.0.24 through v14.0.0 to v15.0.0 introduces a modern factory-function API and pure-fluent signature DSL that eliminate deprecated patterns. Understanding these migration strategies for updating Ax projects ensures your codebase maintains compatibility while gaining stronger compile-time guarantees and improved runtime performance.

Deprecated Patterns and Modern Replacements

Ax v15 removes three legacy patterns in favor of cleaner, type-safe alternatives:

Deprecated Pattern New Replacement Documentation
Template-literal signatures (s\…`/ax`…``) Factory function calls (s('…') / ax('…')) [docs/MIGRATION.md](https://github.com/ax-llm/ax/blob/main/docs/MIGRATION.md#template-literal-migration)
Constructor-based classes (new AxAI(), new AxAgent(), new AxFlow(), new AxRAG()) Factory functions (ai(), agent(), flow(), axRAG()) [docs/MIGRATION.md](https://github.com/ax-llm/ax/blob/main/docs/MIGRATION.md#constructor-migration)
Nested fluent helpers (f.array(f.string()), f.optional(f.string())) Pure-fluent chains (f.string().array(), f.string().optional()) [docs/MIGRATION.md](https://github.com/ax-llm/ax/blob/main/docs/MIGRATION.md#pure-fluent-api-updated-in-v14)

Step-by-Step Migration Workflow

Follow this systematic approach to update your Ax projects from v13 to v15:

  1. Identify deprecated usages using ripgrep to locate legacy patterns:

# Find template literals

rg "ax\`[^`]*\`|s\`[^`]*\`" -t ts

# Find constructor calls

rg "new\s+Ax(AI|Agent|Flow|RAG)" -t ts

# Find nested fluent helpers

rg "f\.(array|optional|internal)\(" -t ts
  1. Execute automated replacements for straightforward migrations:

# Template literals to factory functions

find . -name "*.ts" -exec sed -i 's/ax`\([^`]*\)`/ax("\1")/g' {} \;
find . -name "*.ts" -exec sed -i 's/s`\([^`]*\)`/s("\1")/g' {} \;

# Constructors to factory functions

find . -name "*.ts" -exec sed -i 's/new AxAI(/ai(/g' {} \;
find . -name "*.ts" -exec sed -i 's/new AxAgent(/agent(/g' {} \;
find . -name "*.ts" -exec sed -i 's/new AxRAG(/axRAG(/g' {} \;

# Nested helpers to pure-fluent chains

find . -name "*.ts" -exec sed -i 's/f\.array(/f.string().array(/g' {} \;
find . -name "*.ts" -exec sed -i 's/f\.optional(/f.string().optional(/g' {} \;
find . -name "*.ts" -exec sed -i 's/f\.internal(/f.string().internal(/g' {} \;
  1. Update import statements to reference factory functions:
find . -name "*.ts" -exec sed -i 's/import { AxAI }/import { ai }/g' {} \;
find . -name "*.ts" -exec sed -i 's/import { AxAgent }/import { agent }/g' {} \;
  1. Refactor dynamic signatures manually using the fluent f() builder for runtime-determined field types.

  2. Validate the migration by running the test suite:

npm run test --workspace=@ax-llm/ax
  1. Iterate on any compilation errors until the suite passes.

Architectural Rationale for the New API

The shift to factory functions and pure-fluent chains in Ax v14+ delivers concrete technical advantages:

  • Factory functions (ai(), agent(), flow()) provide the compiler with complete knowledge of concrete LLM configurations at the call site. This improves IntelliSense accuracy and ensures model-specific options are correctly typed according to the source code in src/ax/index.ts.

  • Pure-fluent DSL eliminates runtime parsing overhead. Previously, nested helpers like f.array(f.string()) required order-dependent resolution. The new chainable API (f.string().array().optional()) resolves statically, removing that runtime cost as implemented in the core library.

  • Consistent entry points standardize every top-level object under the same "create-then-use" pattern, simplifying onboarding and future provider extensions.

Common Migration Pitfalls and Fixes

Issue Symptom Resolution
Variable name clash const ai = ai(...) shadows the factory function, causing "ai is not a function" errors Rename the instance (e.g., const llm = ai(...)) as documented in the "Variable Naming Conflicts" section of docs/MIGRATION.md
Dynamic field types TypeScript infers any for dynamically constructed fields Use the fluent f() builder where field types are determined at runtime (see "Template Literal Field Interpolation" in the migration guide)
Missing import updates Compile-time error: "cannot find name 'AxAI'" Ensure all old class imports are swapped for factory functions (ai, agent, etc.) exported from src/ax/index.ts

Before and After Code Examples

Agent Implementation

Pre-migration (v13 style):

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

const ai = new AxAI({ name: "openai", apiKey: process.env.OPENAI_APIKEY! });
const sig = s`question:string -> answer:string`;
const agent = new AxAgent({ name: "helper", signature: sig, ai });

Post-migration (v14+ style):

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

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

// Fluent signature (preferred for dynamic fields)
const sig = f()
  .input("question", f.string("User query"))
  .output("answer", f.string("Generated answer"))
  .build();

const helper = agent({
  name: "helper",
  signature: sig,
  ai: llm,
});

RAG Implementation

Deprecated class-based approach:

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

const ai = new AxAI({ name: "openai", apiKey: "…" });
const rag = new AxRAG({ ai, db: vectorDb });

Modern factory function approach:

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

const llm = ai({ name: "openai", apiKey: "…" });
const rag = axRAG({ ai: llm, db: vectorDb });

Key Reference Files

File Purpose
[docs/MIGRATION.md](https://github.com/ax-llm/ax/blob/main/docs/MIGRATION.md) Authoritative guide covering all deprecated patterns and migration scripts
[src/examples/agent-migration-example.ts](https://github.com/ax-llm/ax/blob/main/src/examples/agent-migration-example.ts) Concrete, up-to-date example of a migrated agent implementation
[src/docs/src/content/docs/migration.md](https://github.com/ax-llm/ax/blob/main/src/docs/src/content/docs/migration.md) Documentation site source for the migration guide
src/ax/index.ts Core library entry point showing exported factory functions (ai, agent, ax, f)
src/ax/**/*.test.ts Test suite for validating migrated code against type contracts

Summary

  • Factory functions replace constructors: Swap new AxAI(), new AxAgent(), and new AxRAG() with ai(), agent(), and axRAG() to enable better type inference and IntelliSense.
  • Function calls replace template literals: Convert s`...` and ax`...` to s('...') and ax('...') for static signatures, or use the fluent f() builder for dynamic fields.
  • Pure-fluent chains replace nested helpers: Transform f.array(f.string()) into f.string().array() to eliminate runtime parsing overhead and enable static resolution.
  • Automated migration is available: Use the provided find and sed commands to bulk-update most patterns, then run npm run test --workspace=@ax-llm/ax to validate.
  • Avoid variable shadowing: Never name your instance const ai = ai(...); use descriptive names like const llm = ai(...) instead.

Frequently Asked Questions

How do I handle dynamic signature fields that were built with template literals?

For signatures where field types are determined at runtime, replace the template literal approach with the fluent f() builder API. Instead of constructing strings dynamically, use f().input() and f().output() with type-safe methods like f.string(), f.number(), and their chainable modifiers (.array(), .optional()). This approach is documented in the "Template Literal Field Interpolation" section of docs/MIGRATION.md.

What is the correct way to rename variables when migrating from constructors to factory functions?

When migrating from new AxAI() to ai(), avoid naming the resulting instance ai as this shadows the factory function import. Instead, use descriptive variable names that indicate the provider or purpose, such as const llm = ai({ name: "openai", ... }) or const openaiClient = ai(...). This prevents "ai is not a function" errors when you attempt to create additional instances later in the same scope.

Are there automated tools to verify my migration is complete?

Yes, after applying the automated find and sed replacements, run the comprehensive test suite using npm run test --workspace=@ax-llm/ax. This executes the library's type contracts and runtime assertions against your migrated code. Additionally, use rg (ripgrep) commands to search for remaining deprecated patterns like new AxAI, ax`, or f.array( to ensure no legacy code remains.

Why were constructor-based classes removed in favor of factory functions?

The shift to factory functions (ai(), agent(), axRAG()) enables the TypeScript compiler to infer concrete LLM configuration types at the call site, providing accurate IntelliSense and ensuring model-specific options are correctly typed. According to the source code in src/ax/index.ts, this pattern standardizes the "create-then-use" approach across all top-level objects, simplifying the API surface and eliminating the runtime overhead associated with class instantiation and this binding.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →