# How to Use the Fluent API for Complex Signatures in Ax: A Complete Guide

> Learn to use Ax's type-safe fluent API to programmatically build complex signatures. Explore chainable validation modifiers and nested object support in this comprehensive guide.

- Repository: [Ax/ax](https://github.com/ax-llm/ax)
- Tags: tutorial
- Published: 2026-02-25

---

**Ax provides a type-safe fluent API (the `f` helper) that enables programmatic construction of complex input-output signatures with chainable validation modifiers and nested object support.**

The **ax-llm/ax** repository implements a sophisticated declarative signature system for LLM interactions. When you need to define intricate request-response contracts beyond simple string templates, the **fluent API for complex signatures in Ax** offers compile-time validation, immutable field builders, and structured output enforcement.

## Understanding the Fluent API Architecture

The fluent API centers on three core components implemented in [`src/ax/dsp/sig.ts`](https://github.com/ax-llm/ax/blob/main/src/ax/dsp/sig.ts). Understanding their interaction is essential for building maintainable, type-safe signatures.

### AxSignatureBuilder

The `AxSignatureBuilder` class acts as a mutable accumulator for field definitions. Located at [`src/ax/dsp/sig.ts#L45-L165`](https://github.com/ax-llm/ax/blob/main/src/ax/dsp/sig.ts#L45-L165), this builder maintains internal arrays for input and output fields through the `input()` and `output()` methods (lines 60-70 and 116-126). Each method accepts a field name, an `AxFluentFieldType` instance, and an optional `prepend` boolean to control field ordering.

### The f Factory Function

The `f` export serves dual purposes as implemented at [`src/ax/dsp/sig.ts#L442-L493`](https://github.com/ax-llm/ax/blob/main/src/ax/dsp/sig.ts#L442-L493). When invoked as a function (`f()`), it instantiates a fresh `AxSignatureBuilder`. As a namespace object, it exposes field-type factories including `f.string()`, `f.number()`, `f.object()`, `f.boolean()`, `f.datetime()`, and `f.class()`.

### AxFluentFieldType and Modifiers

Each field factory returns an `AxFluentFieldType` instance defined at [`src/ax/dsp/sig.ts#L335-L417`](https://github.com/ax-llm/ax/blob/main/src/ax/dsp/sig.ts#L335-L417). This object encapsulates type metadata and provides chainable modifier methods that return **new** immutable instances:

- **`optional()`** – Marks the field as non-required (lines 300-313)
- **`array()`** – Converts the type to an array variant (lines 316-333)
- **`min()` / `max()`** – Applies numeric or length constraints (lines 378-394)
- **`regex()`** – Adds pattern validation (line 395)
- **`email()` / `url()`** – Applies format validators (lines 440-452)
- **`internal()`** – Hides the field from external prompts (lines 334-345)
- **`cache()`** – Enables response caching for the field

## Building Complex Signatures with the Fluent API

The following patterns demonstrate how to construct signatures ranging from simple constrained fields to deeply nested schemas.

### Basic Signatures with Constraints

Define input validation directly in the signature using chained constraint methods. This example creates a user validation schema with email format checking and age boundaries:

```typescript
import { f } from '@ax-llm/ax';

const sig = f()
  .input('email', f.string('User email').email())
  .input('age', f.number('User age').min(18).max(120))
  .input('tags', f.string('Tag').array('List of tags'))
  .output('welcomeMessage', f.string('Greeting text'))
  .output('isAdult', f.boolean('Age-check result'))
  .description('Validates a user profile and returns a greeting')
  .build();

console.log(sig.toString());
// → "email:string.email() age:number.min(18).max(120) tags:string[]. -> welcomeMessage:string isAdult:boolean"

```

*Key source references:* `f.string` (line 462), `.email()` (lines 440-452), `.min()` / `.max()` (lines 378-394), `.array()` (lines 316-333), `builder.build()` (lines 170-186).

### Nested Objects and Optional Fields

Complex payloads require nested object definitions. The `f.object()` factory accepts a map of field names to fluent field definitions, with `ValidateNoMediaTypes<T>` (lines 37-45) preventing media types (`image`, `audio`, `file`) from being nested inside objects at compile time:

```typescript
import { f } from '@ax-llm/ax';

const sig = f()
  .input('request', f.object({
    query: f.string('Search query'),
    context: f.string('Optional context').optional(),
    metadata: f.object({
      source: f.string('Data source').optional(),
      timestamp: f.datetime('When the data was fetched')
    })
  }, 'User request payload'))
  .output('answer', f.string('Answer text'))
  .output('reasoning', f.string('Step-by-step reasoning').internal())
  .build();

console.log(sig.getInputFields());
/* 
[
  { name: 'request', type: { name: 'object', fields: { query: …, context: …, metadata: … } } }
]
*/

```

*Key source references:* `f.object` (lines 620-682), `.optional()` (lines 300-313), `.internal()` (lines 334-345).

### Arrays and Classification Types

Use `f.class()` to constrain values to specific string literals, and chain `.array()` to create collections of complex objects:

```typescript
import { f } from '@ax-llm/ax';

const sig = f()
  .input('text', f.string('Text to classify'))
  .output('sentiment', f.class(['positive', 'negative', 'neutral'] as const, 'Sentiment'))
  .output('entities', f.object({
    type: f.class(['PERSON', 'ORG', 'LOCATION'] as const, 'Entity type'),
    value: f.string('Entity string')
  }).array('Detected entities'))
  .build();

console.log(sig.toString());
// → "text:string -> sentiment:class['positive','negative','neutral'] entities:object[]."

```

*Key source references:* `f.class` (lines 758-788), `.array()` on objects (lines 618-630).

### Enforcing Structured Output

By default, single-output signatures may return raw strings. Call `.useStructured()` before `.build()` to force JSON object wrapping, ensuring downstream parsers always receive valid objects:

```typescript
import { f } from '@ax-llm/ax';

const sig = f()
  .input('prompt', f.string())
  .output('summary', f.string())
  .useStructured()   // guarantees `{ "summary": "…" }`
  .build();

console.log(sig.getOutputFields());
// Output field is still `summary:string`, but the runtime enforces structured JSON.

```

*Key source reference:* `AxSignatureBuilder.useStructured` (lines 202-212).

## Validation and Type Safety

When invoking `.build()` (lines 170-186), `AxSignatureBuilder` performs several validation steps defined in the `validateField` utility:

1. **Uniqueness checks** – Field names must be non-empty and unique across inputs and outputs
2. **Type validation** – Media types are rejected when nested inside objects via `ValidateNoMediaTypes<T>` compile-time checks
3. **Constraint storage** – Validation rules (`min`, `max`, `regex`, `email`, `url`) populate the `AxField.type` metadata for LLM prompting

All validation failures throw `AxSignatureValidationError` with contextual suggestions, preventing runtime signature mismatches.

## Summary

- The **fluent API** (`f`) in [`src/ax/dsp/sig.ts`](https://github.com/ax-llm/ax/blob/main/src/ax/dsp/sig.ts) provides a type-safe, chainable interface for building `AxSignature` instances programmatically.
- **Field modifiers** like `.optional()`, `.array()`, and `.email()` return immutable `AxFluentFieldType` instances, enabling complex constraint composition.
- **Nested objects** support arbitrary depth through `f.object()`, with compile-time guards preventing invalid media type placement.
- **Structured output enforcement** via `.useStructured()` guarantees JSON responses even for single-field outputs.
- The builder validates field uniqueness, type compatibility, and constraint consistency during `.build()`, throwing descriptive errors for invalid configurations.

## Frequently Asked Questions

### How do I mark a field as optional in the Ax fluent API?

Call the `.optional()` method on any field type factory result before passing it to `.input()` or `.output()`. For example: `f.string('Description').optional()`. This modifies the field metadata to indicate the value is not required during validation.

### Can I nest media types like images inside objects using the fluent API?

No. The `ValidateNoMediaTypes<T>` generic constraint at lines 37-45 of [`src/ax/dsp/sig.ts`](https://github.com/ax-llm/ax/blob/main/src/ax/dsp/sig.ts) prevents media types (`image`, `audio`, `file`) from being defined inside nested `f.object()` schemas. Media types must be defined as top-level input fields only.

### What is the difference between `.internal()` and `.optional()` field modifiers?

`.optional()` indicates that a field may be omitted from the LLM's response or input payload, while `.internal()` marks a field as hidden from the external prompt entirely (used for internal reasoning or caching). Internal fields are processed by Ax but not exposed to the language model during generation.

### How do I force JSON output when my signature has only one output field?

Chain `.useStructured()` immediately before calling `.build()` on the `AxSignatureBuilder`. This method, implemented at lines 202-212, sets a flag that forces the runtime to wrap single values in a JSON object, ensuring consistent parsing regardless of the LLM's output format.