This is the full developer documentation for Loom
# LOOM
> lets you build 𝙛𝙖𝙨𝙩 agents 𝙛𝙖𝙨𝙩𝙚𝙧
## Get Started
```bash
$ bun i loom-agents
```
```typescript
import { Agent } from "loom-agents";
const agent = new Agent({
name: "Loomie",
purpose: "You're an AI agent made to automate tasks simply and fast!",
});
const result = await agent.run("Ready to get building?");
console.log(result.final_message);
```
```bash
$ bun index.ts
🚀 Pfft, you kidding? Hell yeah, let's build this thing! 🔥😎
```
## Key Features
Hierarchical Agent Structure
Create specialized agents that can collaborate to solve complex problems.
Advanced Tracing
Track every step of your agent’s execution with detailed, hierarchical traces.
MCP Integration
Leverage third party integrations to take your agents to the next level
Tool Integration
Extend your agents with custom tools for database access, API calls, and more.
OpenAI Compatible
Built to work seamlessly with OpenAI’s latest models and capabilities.
## Core Components
Agent
The core component that interacts with AI models and tools. [Learn more](/docs/reference/agent/)
Trace
Track execution with detailed hierarchical logging. [Learn more](/docs/reference/trace/)
Runner
Execute agents with consistent configuration and error handling. [Learn more](/docs/reference/runner/)
Loom
Global configuration for OpenAI API settings. [Learn more](/docs/reference/loom/)
# Agent
> Core component for building AI assistants with specific purposes and capabilities
The `Agent` class is the primary building block in the Loom framework. It represents an AI assistant with a specific purpose, capable of using tools and generating responses.
## Creating an Agent
```typescript
import { Agent } from "loom-agents";
const agent = new Agent({
name: "Researcher",
purpose: "Find and summarize information",
model: "gpt-4o",
tools: [
// Tool definitions
],
web_search: {
enabled: true,
config: {
search_context_size: "medium",
},
},
});
```
## Agent Configuration
The `AgentConfig` interface defines the available configuration options:
```typescript
interface AgentConfig {
name: string; // Required: Unique name for the agent
purpose: string; // Required: Defines the agent's objective
sub_agents?: Agent[]; // Optional: Sub-agents that can be called
tools?: ToolCall[]; // Optional: Tools the agent can use
model?: string; // Optional: Default is 'gpt-4o'
mcp_servers?: (MCPServerSSE | MCPServerStdio)[];
web_search?: WebSearchConfig; // Optional: Web search configuration
timeout_ms?: number; // Optional: Default is 60000ms (1 minute)
client_config?: ClientOptions; // Optional: A custom OpenAI ClientOptions object for this Agent
api?: "completions" | "responses"; // Optional: Force this client to use a different API than the Loom global config
}
```
## Agent Request and Response
When running an agent, you can use a simple string or a request object:
```typescript
// AgentRequest interface
interface AgentRequest {
context: T[]; // Conversation context
}
// AgentResponse interface
interface AgentResponse {
status: string; // "completed", "error", etc.
final_message: string; // The final output message
context: T[]; // The updated conversation context
}
```
## Running an Agent
Agents can process string inputs or more complex request objects:
```typescript
// Simple string input
const result = await agent.run("Find information about quantum computing");
// Context-aware request
const result = await agent.run(
{
context: [
{
role: "user",
content: "What can you tell me about quantum computing?",
},
{
role: "assistant",
content:
"Quantum computing is a type of computing that uses quantum phenomena...",
},
{ role: "user", content: "How does it compare to classical computing?" },
],
},
optionalTraceObject // Optional: For observability
);
```
## Working with MCP Servers
Loom supports integrating with external tools using the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction), a standard that allows third-party applications and services to provide context and capabilities to LLMs.
You can use two types of MCP connections:
* [`MCPServerSSE`](../reference/mcp) – Connects to an MCP tool server over **Server-Sent Events**
* [`MCPServerStdio`](../reference/mcp) – Connects to a local MCP tool server via **stdio**
### Example: Connecting to MCP Servers
```ts
import { Agent, MCPServerSSE, MCPServerStdio } from "loom-agents";
const researchAgent = new Agent({
name: "MCP Tool Runner",
purpose: "Run a connected MCP tool!",
mcp_servers: [
new MCPServerSSE(new URL("http://localhost:3001/sse")),
new MCPServerStdio("bun", ["stdio.ts"]),
],
});
```
With this configuration, your agent can call tools exposed by the connected MCP servers seamlessly.
## Defining Tools
Tools allow agents to interact with external systems or perform specialized tasks:
```typescript
const tools = [
{
name: "fetchWeather",
description: "Get current weather for a location",
parameters: {
location: {
type: "string",
description: "City name or coordinates",
},
},
callback: async ({ location }) => {
// Implementation to fetch weather data
return { temperature: 72, conditions: "sunny" };
},
},
];
```
## Agents as Tools
Sub agents are great, but sometimes we need to leverage the power of an Agent, while not fully influencing the context of our whole orchestration, that’s where using Agents as Tools comes in.
```typescript
const translationAgent = new Agent({
name: "Translation Agent",
purpose:
"I translate text into different languages, just let me know the language you want to translate to.",
});
const greetingAgent = new Agent({
name: "Greeting Agent",
purpose: "Generate a greeting",
tools: [
translationAgent.asTool({
request: {
type: "string",
description: `The text to translate`,
},
language: {
type: "string",
description: `The language to translate to`,
},
}),
],
});
```
## Advanced Agent Model Configuration
Some model providers make models that are better for certain tasks, so we need a way to Loom configurations to work with these providers.
```typescript
const deepseekAgent = new Agent({
name: "DeekSeek",
purpose: "Are you deepseek or chatgpt? You tell me!",
model: "deepseek-chat",
api: "completions", // we need to override this specific model to use the completions api since deekseek doesn't support responses.
client_config: { // this will create a deepseekAgent specific OpenAI client.
baseURL: "https://api.deepseek.com",
apiKey: "sk-",
},
});
```
## Using Web Search
Agents can perform web searches when configured:
```typescript
const agent = new Agent({
// ... other config
web_search: {
enabled: true,
config: {
search_context_size: "high", // 'high', 'medium', or 'low'
user_location: {
type: "approximate",
country: "US",
city: "San Francisco",
region: "CA",
},
},
},
});
```
## Composing with Sub-agents
Agents can delegate tasks to specialized sub-agents:
```typescript
const mathAgent = new Agent({
name: "MathExpert",
purpose: "Solve complex math problems",
// ... other config
});
const researchAgent = new Agent({
name: "Researcher",
purpose: "Find and summarize information",
sub_agents: [mathAgent],
// ... other config
});
// The researchAgent can now call mathAgent for math problems
```
## Related
* [Runner](/docs/reference/runner): Orchestrates agent execution with enhanced context management
* [Trace](/docs/reference/trace): Provides structured observability for agent operations
* [Loom](/docs/reference/loom): Global configuration for OpenAI settings and API preferences
# Loom
> Global configuration for OpenAI API settings
The `Loom` module provides global configuration for OpenAI API settings. It’s implemented as a singleton that maintains consistent settings across your application.
## Basic Usage
```typescript
import { Loom } from 'loom-agents';
// Configure OpenAI API
Loom.api = 'completions'; // or 'responses'
Loom.openai_config = {
apiKey: process.env.OPENAI_API_KEY,
organization: process.env.OPENAI_ORG_ID
};
// Access OpenAI client
const completion = await Loom.openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello!' }]
});
```
## Configuration Options
The `ConfigOptions` interface defines available configuration:
```typescript
interface ConfigOptions {
api?: 'completions' | 'responses'; // API mode to use
openai_config?: ClientOptions; // OpenAI client configuration
}
```
## Setting API Mode
The `api` property controls which OpenAI endpoint to use:
```typescript
// Use OpenAI's Chat Completions API
Loom.api = 'completions';
// Use OpenAI's Responses API
Loom.api = 'responses';
```
The selected API mode affects how [Agents](/docs/reference/agent) process requests.
## Configuring OpenAI Client
The `openai_config` property accepts any options supported by the OpenAI Node.js client:
# MCPServer
> Base class for connecting to Model Context Protocol-compatible tool servers over SSE or stdio
The `MCPServer` system provides a standardized way to connect to tool servers that implement the [Model Context Protocol (MCP)](https://modelcontext.org). It supports both **SSE (Server-Sent Events)** and **stdio (standard input/output)** transports.
## Creating a Server Connection
There are two built-in classes for connection:
* `MCPServerSSE`: Connects to a remote MCP server using Server-Sent Events.
* `MCPServerStdio`: Launches and communicates with a local MCP server using standard input/output.
```ts
import { MCPServerSSE, MCPServerStdio } from "loom-agents/mcp";
// Using SSE
const mcpSSE = new MCPServerSSE(new URL("http://localhost:3000/mcp"));
// Using stdio
const mcpStdio = new MCPServerStdio("node", ["./tool-server.js"]);
```
## Listing Available Tools
Each server may expose a set of tools that can be queried:
```ts
const tools = await mcpSSE.getTools();
console.log(tools);
// {
// tools: [
// {
// name: "calculate-bmi",
// inputSchema: {
// type: "object",
// properties: [Object],
// required: [Array],
// additionalProperties: false,
// $schema: "http://json-schema.org/draft-07/schema#",
// },
// },
// {
// name: "fetch-weather",
// inputSchema: {
// type: "object",
// properties: [Object],
// required: [Array],
// additionalProperties: false,
// $schema: "http://json-schema.org/draft-07/schema#",
// },
// },
// ];
// }
```
This will automatically connect the client if it hasn’t already.
## Calling a Tool
Once connected, tools can be called directly by name:
```ts
const result = await mcpSSE.callTool({
name: "calculate-bmi",
arguments: {
"weightKg":70,
"heightM":1.75
}
});
console.log(result);
// {
// content: [
// {
// type: "text",
// text: "22.857142857142858",
// },
// ];
// }
// Note, Loom parses MCP results for errors and sends the content / output off `.content`
// [
// {
// type: "text",
// text: "22.857142857142858",
// },
//]
```
## Extending MCPServer
If you want to implement a custom transport or extend tool behaviors, you can subclass `MCPServerBase`.
```ts
class CustomMCPServer extends MCPServerBase {
constructor(myTransport: CustomTransport) {
super(myTransport);
}
// Custom behavior
}
```
## Internals
The `MCPServerBase` manages client connection state and handles listing and calling tools. It lazily initializes the client only when needed:
```ts
await this.client.connect(this.transport);
```
It caches tool metadata after the first call to `getTools()` to avoid redundant lookups.
## Related
* [Agent](/docs/reference/agent): Interacts with tools through an MCPServer
* [Tool Definition](/docs/reference/tool): Learn how tools are defined and exposed
* [Trace](/docs/reference/trace): Structured tracking of tool calls and agent interactions
* [Loom Configuration](/docs/reference/loom): Customize global AI behavior and context handling
# Runner
> Orchestrates agent execution and manages multi-turn interactions
The `Runner` class orchestrates the execution of [Agents](/docs/reference/agent), handling multi-turn interactions and providing execution context.
## Creating a Runner
```typescript
import { Agent, Runner } from "loom-agents";
const agent = new Agent({
name: "Assistant",
purpose: "Help with general tasks",
// ... other config
});
const runner = new Runner(agent, {
name: "MainRunner",
max_depth: 5,
context: {
user_id: "user-123",
session_start: Date.now(),
},
});
```
## Runner Configuration
The `RunnerConfig` interface defines available configuration options:
```typescript
interface RunnerConfig {
max_depth?: number; // Optional: Maximum number of turns (default: 10)
name?: string; // Optional: Runner name (default: "Runner")
id?: string; // Optional: Unique ID (default: auto-generated UUID)
context?: Record; // Optional: Additional context data
}
```
## Running an Agent
The `run` method executes the agent and manages the interaction flow:
```typescript
// Simple string input
const result = await runner.run("Help me write a resume");
// Rich context input
const result = await runner.run({
context: [
{ role: "user", content: "I need help with my resume" },
{ role: "assistant", content: "I can help! What industry are you in?" },
{ role: "user", content: "Software engineering" },
],
});
```
## Managing Execution Flow
The Runner handles:
1. Executing the agent with the provided input
2. Processing agent responses and tool calls
3. Managing multi-turn conversations up to `max_depth`
4. Tracing execution with the integrated [Tracer](/docs/reference/trace)
## Handling Results
The `run` method returns a `RunnerResponse` which is a souped-up [AgentResponse](/docs/reference/agent)
```typescript
interface RunnerResponse extends AgentResponse {
trace: Trace; // The execution trace
}
```
## Accessing Execution Data
The Runner provides methods to access execution details:
```typescript
// Get the most recent trace
const lastTrace = runner.getLastTrace();
// Get all traces from this runner
const traces = runner.getTraces();
// Render the last trace for debugging
console.log(lastTrace.render());
```
## Execution Depth and Handling
The Runner prevents infinite loops by enforcing a maximum execution depth:
```typescript
const runner = new Runner(agent, {
max_depth: 5, // Agent will stop after 5 interaction turns
});
```
If the maximum depth is reached before the agent completes its task, the runner will return the current state with the status from the agent’s last response.
## Related
* [Agent](/docs/reference/agent): The core AI component orchestrated by Runners
* [Trace](/docs/reference/trace): Provides detailed execution logs
* [Loom](/docs/reference/loom): Global configuration for OpenAI settings
# Trace
> Structured logging and performance monitoring for agent operations
The `Trace` module provides structured logging and performance tracking for agent operations, making it easier to debug, analyze, and optimize agent behavior.
## Trace Classes
The module consists of two main classes:
* `Trace`: Represents an individual trace entry with hierarchical structure
* `TraceDetails`: Interface defining the structure of trace data
## Creating a Trace
Traces can be created directly for standalone use:
```typescript
import { Trace } from 'loom-agents';
const rootTrace = new Trace('main-operation', {
input: 'user query',
timestamp: Date.now()
});
```
## Adding Sub-traces
Traces can be nested to represent hierarchical operations:
```typescript
// Start a child trace
const parseTrace = rootTrace.start('parse-input', {
parser: 'default',
input_length: input.length
});
// Perform operation
const result = parseInput(input);
// End the trace (returns duration in ms)
const duration = parseTrace.end();
console.log(`Parsing took ${duration}ms`);
```
## The TraceDetails Interface
The `TraceDetails` interface defines the structure of trace data:
```typescript
interface TraceDetails {
name: string; // Operation name
uuid: string; // Unique trace identifier
data: any; // Associated metadata
startTime: number; // Timestamp when trace started
endTime?: number; // Timestamp when trace ended (if completed)
children: TraceDetails[]; // Child traces
}
```
## Getting Trace Details
You can retrieve the full trace data structure:
```typescript
// Get the complete trace hierarchy
const traceData = rootTrace.getDetails();
// The structure includes all child traces
console.log(JSON.stringify(traceData, null, 2));
```
## Rendering Traces
The `render` method produces a formatted tree representation of the trace:
```typescript
// Generate human-readable trace output
const traceOutput = rootTrace.render();
console.log(traceOutput);
```
Example output:
```plaintext
[trace.123e4567-e89b-12d3-a456-426614174000] main-operation (1200 ms) - {"input":"query"}
└─ [trace.234e5678-e89b-12d3-a456-426614174001] parse-input (50 ms) - {"parser":"default"}
├─ [trace.345e6789-e89b-12d3-a456-426614174002] tokenize (20 ms) - {"tokens":15}
└─ [trace.456e7890-e89b-12d3-a456-426614174003] process (1000 ms) - {"model":"gpt-4o"}
```
## Integration with Agents
The trace system is integrated with [Agent](/docs/reference/agent) for detailed operation tracking:
```typescript
// Create a trace for agent operations
const trace = new Trace('agent-execution', { agent: agent.uuid });
// Pass the trace to the agent's run method
const result = await agent.run(input, trace);
// Analyze the trace after execution
console.log(trace.render());
```
## Integration with Runner
The [Runner](/docs/reference/runner) class automatically creates and manages traces:
```typescript
// Create a runner
const runner = new Runner(agent);
// Run the agent
const result = await runner.run(input);
// Get the runs trace
const trace = result.trace;
// Or get the last trace for the runner
const lastTrace = runner.getLastTrace();
console.log(lastTrace.render());
// Or get all traces the runner has made
const allTraces = runner.getTraces();
```
## Performance Analysis
Traces can be used to identify performance bottlenecks:
```typescript
// Get total execution time
const totalTime = rootTrace.end(); // Returns total duration in ms
```
## Related
* [Agent](/docs/reference/agent): Core component that generates traces during execution
* [Runner](/docs/reference/runner): Orchestrates agents and manages trace hierarchy
* [Loom](/docs/reference/loom): Global configuration
# Examples
> Usage examples for the Loom framework
This page provides practical examples of using the Loom framework for various agent-based applications.
## Basic Agent
```typescript
import { Agent, Runner, Loom } from 'loom-agents';
// Configure Loom
Loom.api = 'completions';
Loom.openai_config = { apiKey: process.env.OPENAI_API_KEY };
// Create an agent
const agent = new Agent({
name: 'Assistant',
purpose: 'Help with general questions',
model: 'gpt-4o'
});
// Create a runner
const runner = new Runner(agent);
// Execute the agent
const result = await runner.run('What is the capital of France?');
console.log(result.final_message); // "The capital of France is Paris."
```
## Using Tools
```typescript
import { Agent, Runner } from 'loom-agents';
// Define tool
const weatherTool = {
name: 'getWeather',
description: 'Get current weather for a location',
parameters: {
location: {
type: 'string',
description: 'City name or coordinates'
}
},
callback: async ({ location }) => {
// In a real implementation, this would call a weather API
return { temperature: 72, conditions: 'sunny' };
}
};
// Create an agent with the tool
const agent = new Agent({
name: 'WeatherAssistant',
purpose: 'Provide weather information',
model: 'gpt-4o',
tools: [weatherTool]
});
const runner = new Runner(agent);
const result = await runner.run('What\'s the weather like in San Francisco?');
console.log(result.final_message);
```
## Composing Agents
```typescript
import { Agent, Runner } from 'loom-agents';
// Create specialized agents
const mathAgent = new Agent({
name: 'MathExpert',
purpose: 'Solve math problems',
model: 'gpt-4o'
});
const weatherAgent = new Agent({
name: 'WeatherExpert',
purpose: 'Provide weather information',
model: 'gpt-4o',
tools: [/* weather tools */]
});
// Create a coordinator agent
const coordinatorAgent = new Agent({
name: 'Coordinator',
purpose: 'Route requests to appropriate specialized agents',
model: 'gpt-4o',
sub_agents: [mathAgent, weatherAgent]
});
// Run the coordinator
const runner = new Runner(coordinatorAgent);
const result = await runner.run('Can you tell me the weather in NYC and also calculate 15% of 85?');
console.log(result.final_message);
```
## Using Traces for Debugging
```typescript
import { Agent, Runner } from 'loom-agents';
// Create agent and runner
const agent = new Agent({ /*...*/ });
const runner = new Runner(agent);
// Execute with tracing
const result = await runner.run('Process this data');
// Get and display the trace
const lastTrace = runner.getLastTrace();
console.log(lastTrace.render());
// Or get all traces from this runner
const allTraces = runner.getTraces();
```
## Web Search Agent
```typescript
import { Agent, Runner } from 'loom-agents';
// Create an agent with web search enabled
const researchAgent = new Agent({
name: 'Researcher',
purpose: 'Find information online',
model: 'gpt-4o',
web_search: {
enabled: true,
config: {
search_context_size: 'high',
user_location: {
type: 'approximate',
country: 'US'
}
}
}
});
const runner = new Runner(researchAgent);
const result = await runner.run('What were the major tech news stories this week?');
console.log(result.final_message);
```
## Related
* [Agent](/docs/reference/agent): Core agent implementation
* [Runner](/docs/reference/runner): Agent execution orchestration
* [Trace](/docs/reference/trace): Execution tracing and debugging
* [Loom](/docs/reference/loom): Global configuration
# Introduction to Loom
> An overview of the Loom framework for building intelligent agents
Loom is a TypeScript framework for building, orchestrating, and tracing AI agents with OpenAI’s API.
## Core Concepts
Loom provides several key abstractions for building agent-based applications:
* **Agent**: A configurable entity that can process inputs, use tools, and produce outputs
* **Runner**: Orchestrates execution of agents, handling multi-turn interactions
* **Trace**: Provides structured logging and performance tracking
* **Loom**: Global configuration for OpenAI API settings
## Getting Started
```typescript
import { Agent, Runner, Loom } from 'loom-agents';
// Configure global settings
Loom.api = 'completions'; // or 'responses'
Loom.openai_config = { apiKey: process.env.OPENAI_API_KEY };
// Create an agent
const agent = new Agent({
name: 'Calculator',
purpose: 'Solve mathematical problems',
model: 'gpt-4o',
tools: [
// Tool definitions
]
});
// Run the agent
const runner = new Runner(agent);
const result = await runner.run('Calculate 23 * 17');
console.log(result.final_message);
```
## Framework Architecture
Loom is designed around a modular architecture:
* **[Agent](/docs/reference/agent)**: The primary abstraction for AI capabilities
* **[Runner](/docs/reference/runner)**: Manages execution flow and handles agent communication
* **[Trace](/docs/reference/trace)**: Provides detailed execution logs and timing information
* **[Loom](/docs/reference/loom)**: Global configuration singleton for OpenAI settings
## Key Features
* Support for both OpenAI’s Chat Completions and Responses APIs
* Tool definition and execution
* Sub-agent composition
* Tracing and observability
* Web search integration
# Loom Schema
> A concise yet complete guide to building and validating JSON Schemas with Loom Schema.
**Loom Schema** is a lightweight toolkit for composing [JSON Schema](https://json-schema.org/) definitions in a fluent manner and validating data against those schemas. It uses [AJV](https://ajv.js.org/) under the hood to perform validation, but abstracts away much of the complexity. You can define schemas for your data, then either compile and validate against them or export them as standard JSON Schemas.
## Features
* **Easy Schema Composition**: Write and reuse fragments for strings, numbers, arrays, objects, etc.
* **Built-in Validation**: Any schema fragment can validate data directly, returning helpful errors if the validation fails.
* **AJV Integration**: Fine-tune validation with AJV’s options, custom keywords, or custom vocabularies.
* **TypeScript-Friendly**: Infer TypeScript types from your Loom schemas, ensuring that your data usage aligns with the rules you set.
## Installation
```bash
bun install loom-schema
```
## Basic Usage Example
```ts
import { object, string, number, Infer } from "loom-schema";
// Define a schema fragment for a user object
const userSchema = object({
properties: {
name: string({ minLength: 1 }),
age: number({ minimum: 0 }),
},
required: ["name", "age"],
});
// Validate some data
async function runExample() {
const result = await userSchema.validate({ name: "Alice", age: 25 });
if (result.valid) {
console.log("Data is valid!");
} else {
console.error("Validation errors:", result.errors);
}
}
runExample();
// Infer TypeScript types from your schema
type User = Infer;
```
In the snippet above:
* We create a simple user schema describing an object with two properties (`name` and `age`).
* We call `.validate(...)` on the schema to check if a sample data object is valid.
* We also demonstrate how to use the `Infer` utility type to derive a TypeScript type from our schema.
Continue to the [API Reference](./api) for details on each helper function.
# API Reference
> Detailed reference for Loom Schema's public API.
This document details each publicly exposed function and type in **Loom Schema**. You will see how to create different schema fragments, compose them, and perform validation.
***
## Core Concepts
### Schema Fragments
Each function (e.g., `string()`, `object()`, `array()`) returns an object we call a **schema fragment**. A **schema fragment** has two main methods:
1. **`validate(data, ajvOptions?)`**\
Validates the input `data` against this schema. Returns a Promise resolving to:
```ts
{
valid: boolean;
errors: ErrorObject[] | null | undefined;
}
```
If `valid` is `false`, `errors` will contain an array of detailed validation issues.
2. **`toSchema(name?)`**\
Generates a valid JSON Schema (as a plain JavaScript object) from this fragment. The optional `name` parameter sets a `$id` and a `title` in the resulting JSON Schema to help identify it.
#### TypeScript Integration
Most schema-building functions allow you to infer the TypeScript type for your schema using:
```ts
type MyType = Infer;
```
This helps ensure your data usage remains consistent with the validation rules you’ve defined.
***
## Base Schema Types
Every schema fragment in **Loom Schema** inherits a common set of base properties defined in the `BaseSchemaOptions` interface. These properties provide metadata, identification, and additional capabilities to your schemas. They are available regardless of the specific type (e.g. string, number, object, etc.) you create, ensuring a consistent foundation across all schema fragments.
**Inherited Base Properties:**
* **`$schema`**: *(optional)* The JSON Schema version identifier (e.g. `"http://json-schema.org/draft/2020-12/schema"`).
* **`$id`**: *(optional)* A unique identifier for the schema, generated automatically if not provided.
* **`$vocabulary`**: *(optional)* Specifies which vocabularies the schema uses.
* **`$comment`**: *(optional)* A comment for documentation purposes.
* **`$anchor`**: *(optional)* Defines a URI fragment identifier to reference this schema.
* **`$recursiveAnchor`**: *(optional)* Indicates if recursive references are allowed.
* **`$recursiveRef`**: *(optional)* A reference to a schema for recursive definitions.
* **`title`**: *(optional)* A human-friendly title for the schema.
* **`description`**: *(optional)* A detailed explanation of what the schema represents.
* **`examples`**: *(optional)* An array of sample values conforming to the schema.
* **`deprecated`**: *(optional)* A boolean flag indicating whether the schema is deprecated.
* **`readOnly`**: *(optional)* A boolean flag indicating the schema is read-only.
* **`writeOnly`**: *(optional)* A boolean flag indicating the schema is write-only.
* **`default`**: *(optional)* A default value for the schema.
* **Composition Keywords**:
* **`allOf`**, **`anyOf`**, **`oneOf`**: Allow combining multiple schemas.
* **`not`**: Excludes a specific schema.
* **`if`**, **`then`**, **`else`**: Enable conditional schema definitions.
* **`$ref`**: *(optional)* A reference to another schema.
* **`$defs`**: *(optional)* An object containing subschemas.
* **`enum`**: *(optional)* An array of possible constant values.
* **`const`**: *(optional)* A constant value that the data must match.
These base properties are automatically mixed into every fragment you build (using helpers like `string()`, `number()`, `object()`, etc.), ensuring that your generated JSON Schemas carry all the necessary metadata and compositional abilities.
## Schema-Building Functions
### `string(options?)`
Creates a **string** schema fragment.
```ts
import { string } from "loom-schema";
const nameSchema = string({ minLength: 1, maxLength: 100 });
```
**Options** (selected common fields):
* `type?: "string"` (internally set to `"string"`)
* `format?: string` — e.g., `"email"`, `"uri"`, etc.
* `minLength?: number`
* `maxLength?: number`
* `pattern?: string`
***
### `number(options?)`
Creates a **number** schema fragment.
```ts
import { number } from "loom-schema";
const scoreSchema = number({ minimum: 0, maximum: 100 });
```
**Options** (selected common fields):
* `type?: "number"` (internally set to `"number"`)
* `minimum?: number`
* `maximum?: number`
* `exclusiveMinimum?: number`
* `exclusiveMaximum?: number`
* `multipleOf?: number`
***
### `integer(options?)`
Creates an **integer** schema fragment (same as number, but `type` set to `"integer"`).
```ts
import { integer } from "loom-schema";
const countSchema = integer({ minimum: 0, maximum: 10 });
```
**Options** (similar to `number`, except `type` is forced to `"integer"`).
***
### `boolean(options?)`
Creates a **boolean** schema fragment.
```ts
import { boolean } from "loom-schema";
const toggleSchema = boolean();
```
**Options** (selected common fields):
* `type?: "boolean"` (internally set to `"boolean"`)
***
### `nil(options?)`
Creates a **null** schema fragment.
```ts
import { nil } from "loom-schema";
const nullOnlySchema = nil();
```
**Options** (selected common fields):
* `type?: "null"` (internally set to `"null"`)
***
### `array(options)`
Creates an **array** schema fragment.
```ts
import { array, number } from "loom-schema";
// An array of numbers
const numberArraySchema = array({ items: number() });
```
**Options**:
* `type?: "array"` (internally set to `"array"`)
* `items?: JsonSchemaInput` — A fragment or a plain object describing the array items.
* `minItems?: number`
* `maxItems?: number`
* `uniqueItems?: boolean`
* And other JSON Schema keywords relevant to arrays.
You can pass another Loom fragment to `items` (like `number()`) or use any other schema you have created.
***
### `object(options)`
Creates an **object** schema fragment.
```ts
import { object, string, number } from "loom-schema";
const userSchema = object({
properties: {
name: string({ minLength: 1 }),
age: number({ minimum: 0 }),
},
required: ["name", "age"],
});
```
**Options**:
* `type?: "object"` (internally set to `"object"`)
* `properties?: Record`
* `required?: string[]`
* `additionalProperties?: JsonSchemaInput | boolean`
* And other JSON Schema keywords relevant to objects.
If you simply pass key-value pairs (without an explicit `properties` field) and do not use reserved keywords, Loom automatically treats them as `properties`.
***
### `schema(options?)`
Creates an **object** schema fragment with an easy way to add multiple possible keywords. It can handle advanced usage (like adding `allOf`, `anyOf`, `oneOf`, etc.) while giving you a convenient single entry point.
```ts
import { schema } from "loom-schema";
const advancedObject = schema({
type: "object",
properties: {
exampleKey: { type: "string" },
},
required: ["exampleKey"],
});
```
Most of the time, you will prefer using the dedicated `object`, `string`, `number`, etc. helpers. But `schema()` is a good fallback if you need full manual control.
* `schema` accepts fragments just like any other fragment would, as-well.
***
## Composition Functions
Sometimes, your data may need to satisfy multiple schemas at once or any of several schemas. Loom offers convenient wrappers for these cases.
### `allOf([...fragments], options?)`
Creates a fragment that requires **all** of the given fragments to be satisfied.\
Returns a single fragment that merges the constraints of each included fragment.
```ts
import { allOf, string, object } from "loom-schema";
// A string that must be at least 3 characters, and must match a pattern
const allOfSchema = allOf([
string({ minLength: 3 }),
string({ pattern: "^[a-zA-Z0-9]+$" }),
]);
```
### `anyOf([...fragments], options?)`
Creates a fragment that requires **at least one** of the given fragments to be satisfied.\
Useful for data that can be of multiple permitted shapes.
```ts
import { anyOf, string, number } from "loom-schema";
const multiTypeSchema = anyOf([string(), number()]);
```
### `oneOf([...fragments], options?)`
Creates a fragment that requires **exactly one** of the given fragments to be satisfied.\
This is stricter than `anyOf`, enforcing that only one fragment may match.
```ts
import { oneOf, object, string } from "loom-schema";
const exclusiveTypeSchema = oneOf([
string({ format: "email" }),
object({
properties: { localPart: string(), domain: string() },
required: ["localPart", "domain"],
}),
]);
```
### `conditional({ if, then, else }, options?)`
Creates a fragment that conditionally applies different schemas based on an `if` condition.\
If data matches the `if` schema, it must also match `then`. Otherwise, it must match `else`.
```ts
import { conditional, object, number, string } from "loom-schema";
const conditionalSchema = conditional({
if: object({ required: ["isSpecial"] }),
then: object({
properties: { isSpecial: number() },
required: ["isSpecial"],
}),
else: string({ format: "email" }),
});
```
***
## Validation
Every fragment has a `validate(data, ajvOptions?)` method:
```ts
const result = await myFragment.validate(someData);
if (!result.valid) {
console.error(result.errors);
} else {
console.log("Data is valid!");
}
```
* **`data`**: The data you want to validate.
* **`ajvOptions`**: Optional settings to pass through to AJV, including:
* `customKeywords` — An array of custom keyword definitions or names.
* `customVocabularies` — Additional vocabularies to include in validation.
* Most other standard AJV options like `allErrors`, `strict`, etc.
***
## Generating JSON Schemas
Each fragment also has a `toSchema(name?)` method that outputs a valid JSON Schema. The optional `name` gets used as `$id` and `title`.
```ts
const rawSchema = myFragment.toSchema("MyAwesomeSchema");
console.log(JSON.stringify(rawSchema, null, 2));
```
This is especially useful when you need to store or share the generated JSON Schema, or integrate with other tooling that expects a standard JSON Schema.
***
## Type Utilities
### `Infer`
A TypeScript helper that extracts the type from any Loom schema fragment. For instance:
```ts
import { object, string, Infer } from "loom-schema";
const userSchema = object({
properties: { name: string() },
required: ["name"],
});
// The TS type is { name: string }
type User = Infer;
```
This ensures that when you use `userSchema` for validation, you also get a consistent TypeScript type for that data.
***
## Example: Putting It All Together
```ts
import { object, string, number, array, anyOf, Infer } from "loom-schema";
// Define a user schema
const userSchema = object({
properties: {
username: string({ minLength: 3 }),
favoriteThings: array({ items: string() }),
age: anyOf([
number({ minimum: 13 }),
number({ multipleOf: 2, maximum: 12 }), // Possibly allow even ages under 13 (just a silly example)
]),
},
required: ["username", "age"],
});
// Validate a data sample
async function example() {
const sample = { username: "abc", age: 14, favoriteThings: ["pizza"] };
const result = await userSchema.validate(sample);
if (result.valid) {
console.log("Data is valid!");
} else {
console.error("Validation errors:", result.errors);
}
}
example();
// Infer the TS type
type User = Infer; // { username: string; age: number; favoriteThings?: string[] }
```
***
## Summary
Loom Schema empowers you to define modular, composable JSON Schemas in TypeScript or JavaScript. Each fragment can be validated independently or turned into a standard JSON Schema. Combining fragments is straightforward with functions like `allOf`, `anyOf`, `oneOf`, and `conditional`.
For more advanced usage, you can pass AJV-specific options to `validate(...)` or generate named schemas via `toSchema("MyName")`. If you are looking to unify your data models and validations, Loom Schema provides a flexible, type-safe solution.