Building with LLMs means wrestling with inconsistent outputs, writing custom parsers, and recreating UI components for every schema. llm-schema eliminates this friction by treating your data structure as the single source of truth.
// 1. Define your schema onceconstschema=defineSchema({title: text(),summary: md(), ... });// 2. Generate response structure instructionsconstresponseFormat=schema.toPrompt();// 3. Build your complete prompt (domain logic + response structure)constprompt=`You are an expert meeting analyst. Analyze the transcript and extract:- Key discussion points- Action items with clear owners- Overall meeting sentiment${responseFormat}`;// 4. Parse and validate LLM responsesconstdata=schema.parse(llmResponse);// 5. Render with zero custom code<SchemaRendererschema={schema}data={data}/>
Just as Prisma and Drizzle abstract away database complexity, llm-schema abstracts away LLM integration complexity.
🎯 Schema-Driven Everything
One definition powers prompts, validation, TypeScript types, React components, and data utilities.
md() fields retain rich formatting while staying type-safe. No more guessing how to render content.
Constrain LLM outputs to specific values. The model sees the options in prompts, validation enforces them.
⚛️ React Components Included
Drop-in <SchemaRenderer> and <SchemaEditor> components with hooks for instant UI.
🔄 Transformation Pipeline
Built-in diff, merge, search, and entity extraction for building workflows and audit trails.
Export to OpenAI tools, Anthropic tools, or JSON Schema. Works with any LLM.
npm install llm-schema
# or
pnpm add llm-schema
# or
yarn add llm-schema
import{defineSchema,text,md,array,entity,enumType,date,number,typeInferSchema}from'llm-schema';constMeetingNotesSchema=defineSchema({title: text({description: 'Concise meeting title'}),summary: md({description: 'High-level recap with **markdown** support',optional: true,maxLength: 2000}),actionItems: array({description: 'Concrete actions with owners',schema: {task: text({description: 'What needs to happen'}),owner: entity('person',{description: 'Person responsible'}),status: enumType(['todo','in-progress','done']asconst,{description: 'Current status',default: 'todo'}),dueDate: date({optional: true,format: 'date'})}}),sentiment: enumType(['positive','neutral','negative']asconst,{description: 'Overall meeting sentiment'}),confidence: number({description: 'Confidence score (0-1)',min: 0,max: 1,optional: true})},{name: 'MeetingNotes',description: 'Structured meeting summary with markdown and action tracking',strict: true});// Get fully typed interfacetypeMeetingNotes=InferSchema<typeofMeetingNotesSchema>;
2. Build Your Complete Prompt
// Generate the response structure partconstresponseFormat=MeetingNotesSchema.toPrompt({format: 'detailed',includeExamples: true});// Build your complete prompt: domain instructions + response structureconstfullPrompt=`You are an expert meeting analyst. Extract key information from meeting transcripts.Focus on:- Concrete action items with clear owners- Overall sentiment and key discussion points- Use markdown formatting in the summary (headers, bold text, lists)${responseFormat}Analyze the meeting transcript and return valid JSON.`;
Concrete example of generated response format:
RESPONSE FORMAT:
Return a JSON object with the following structure:
{
"title": string (required)
Description: Concise meeting title
"summary": markdown string (optional, max 2000 characters)
Description: High-level recap with **markdown** support
Note: Use markdown formatting - headers (##), bold (**text**), lists, etc.
"actionItems": array (required)
Description: Concrete actions with owners
Items: [{
"task": string (required) - What needs to happen
"owner": person entity (required) - Person responsible (email, name, or ID)
"status": enum (required) - Must be one of: "todo", "in-progress", "done" (default: "todo")
"dueDate": ISO date string (optional) - Format: YYYY-MM-DD
}]
"sentiment": enum (required)
Description: Overall meeting sentiment
Must be exactly one of: "positive", "neutral", "negative"
"confidence": number (optional, range: 0-1)
Description: Confidence score (0-1)
}
CRITICAL ENUM VALUES:
- sentiment: Only use "positive", "neutral", or "negative"
- status: Only use "todo", "in-progress", or "done"
EXAMPLE:
{
"title": "Q4 Planning",
"sentiment": "positive",
"actionItems": [{
"task": "Draft budget proposal",
"owner": "[email protected]",
"status": "todo",
"dueDate": "2025-11-15"
}]
}
The response format is generated automatically from your schema definition. You write your domain-specific instructions, and llm-schema handles the structure specification.
importOpenAIfrom'openai';constopenai=newOpenAI();asyncfunctionanalyzeMeeting(transcript: string){constresponseFormat=MeetingNotesSchema.toPrompt({format: 'detailed'});// Your complete prompt = domain instructions + response formatconstsystemPrompt=`You are an expert meeting analyst. Extract structured information from meeting transcripts.Focus on:- Action items with clear ownership and deadlines- Overall meeting tone and key outcomes- Use markdown in the summary for better readability${responseFormat}`;constcompletion=awaitopenai.chat.completions.create({model: 'gpt-4o',messages: [{role: 'system',content: systemPrompt},{role: 'user',content: `Analyze this meeting:\n\n${transcript}`}],response_format: {type: 'json_object'}});constraw=completion.choices[0].message?.content??'{}';// Parse with validation and detailed error messagesconstresult=MeetingNotesSchema.safeParse(raw);if(!result.success){result.issues.forEach(issue=>{console.warn(`${issue.path.join('.')}: ${issue.message}`);});returnnull;}returnresult.data;// Fully typed MeetingNotes!}
The LLM response might look like this:
{
"title": "Q4 Product Planning",
"summary": "## Key Decisions\n\nWe finalized the **Q4 roadmap** with three major initiatives:\n\n1. **Mobile app redesign** - New UI/UX launching in November\n2. **API v2 release** - Breaking changes, migration guide ready\n3. **Enterprise features** - SSO and advanced analytics\n\n### Timeline\n\n- Nov 1: Design review\n- Nov 15: Beta release\n- Dec 1: General availability\n\n**Next meeting:** Progress check-in on Nov 8th",
"actionItems": [
{
"task": "Complete mobile app mockups",
"owner": "[email protected]",
"status": "in-progress",
"dueDate": "2025-11-01"
},
{
"task": "Write API v2 migration guide",
"owner": "[email protected]",
"status": "todo",
"dueDate": "2025-11-10"
}
],
"sentiment": "positive",
"confidence": 0.92
}
Render with zero custom code:
import{SchemaRenderer}from'llm-schema/react';functionMeetingNotesView({ data }: {data: MeetingNotes}){return(<SchemaRendererschema={MeetingNotesSchema}data={data}config={{layout: 'stack',showOptionalFields: true}}/>);}
The rendered output looks like this:
Title: Q4 Product Planning
Summary:
We finalized the Q4 roadmap with three major initiatives:
Mobile app redesign - New UI/UX launching in November
API v2 release - Breaking changes, migration guide ready
The magic: The markdown field is automatically rendered with proper formatting. No custom parsing, no manual HTML generation. Just define md() in your schema and it works.
// Diff two versions for audit trailsconstdiff=MeetingNotesSchema.diff(before,after);// Merge updates (e.g., human-in-the-loop editing)constmerged=MeetingNotesSchema.merge(original,updates);// Search across all text and markdown fieldsconstresults=MeetingNotesSchema.search(data,'budget proposal');// Extract all entities of a specific typeconstpeople=MeetingNotesSchema.getEntities(data,'person');// Get all markdown content for processingconstmarkdown=MeetingNotesSchema.getMarkdownFields(data);
Customer Feedback Analysis
constFeedbackSchema=defineSchema({customerName: entity('person'),feedbackText: md({description: 'Original feedback with quotes'}),sentiment: enumType(['positive','neutral','negative']asconst),categories: array({schema: {category: enumType(['feature-request','bug','praise','complaint']asconst),priority: enumType(['low','medium','high']asconst)}}),actionRequired: boolean({description: 'Does this need follow-up?'}),suggestedResponse: md({optional: true})});
number(options?) - Numeric values with optional min/max
boolean(options?) - True/false values
date(options?) - ISO date strings
enumType(values, options?) - Constrained choice from list
entity(type, options?) - Named entities (person, company, etc.)
array(options) - Lists of items
object(fields) - Nested structures
Common Options:
description - Instructions for the LLM
optional - Whether field can be omitted
default - Default value if not provided
examples - Example values for LLM guidance
schema.toPrompt(options?)// Generate LLM instructionsschema.toOpenAITool(options?)// OpenAI function calling formatschema.toAnthropicTool(options?)// Anthropic tool formatschema.toJsonSchema()// Standard JSON Schemaschema.parse(input)// Parse and validate (throws on error)schema.safeParse(input)// Parse with error detailsschema.diff(v1,v2)// Compare two versionsschema.merge(base,updates)// Merge changesschema.search(data,query)// Full-text searchschema.getEntities(data,type?)// Extract entitiesschema.getMarkdownFields(data)// Get all markdown content