Private BetaProposeFlow is currently in private beta.Join the waitlist

Generating Proposals

Learn how to generate AI proposals from natural language inputs with full control over the output.

Basic Generation

Call generate() with a schema name and natural language input.

generate.ts
const { proposal, generation } = await pf.generate('blogPost', {
  input: 'Write a blog post about the future of AI in healthcare',
});

console.log(proposal.generatedObject);
// {
//   title: "AI in Healthcare: Transforming Patient Care",
//   content: "Artificial intelligence is revolutionizing...",
//   tags: ["ai", "healthcare", "technology"]
// }

console.log(generation.latencyMs);  // 1523

User Attribution (Subject)

Use the subject field to attribute proposals to specific users. This enables user activity analytics on the dashboard.

subject.ts
const { proposal } = await pf.generate('recipe', {
  input: 'Create a pasta recipe',
  subject: { userId: 'user_123' },
});

// Subject is stored with the proposal for analytics
console.log(proposal.subject);  // { userId: 'user_123' }

Adding Metadata

Attach metadata to proposals for tracking and filtering.

metadata.ts
const { proposal } = await pf.generate('blogPost', {
  input: 'Write a blog post about TypeScript',
  metadata: {
    projectId: 'proj_456',
    requestedBy: 'content-team',
  },
});

// Metadata is stored with the proposal
console.log(proposal.metadata.projectId);  // 'proj_456'

Suggestion Metadata

Request AI-generated descriptions and justifications to help users understand the proposal.

suggestion-meta.ts
const { proposal, suggestionMeta } = await pf.generate('blogPost', {
  input: 'Write about React hooks',
  generateSuggestionMeta: true,
});

console.log(suggestionMeta);
// {
//   description: "A comprehensive guide to React hooks...",
//   justification: "This post covers useState, useEffect, and custom hooks..."
// }

// Display to users to help them evaluate the proposal

Suggested Responses

Request AI-generated feedback suggestions that users can click to quickly refine the proposal. These are short phrases like "Make it healthier" or "Add more details".

suggested-responses.ts
const { proposal, suggestedResponses } = await pf.generate('recipe', {
  input: 'A quick pasta dinner',
  generateSuggestedResponses: true,
});

console.log(suggestedResponses);
// ["Make it spicier", "Add more vegetables", "Make it vegan", "Reduce cooking time"]

// Display as clickable pills in your UI
// When clicked, regenerate with that feedback:
const { proposal: improved } = await pf.regenerate(proposal.id, {
  feedback: 'Make it spicier',
});

Generation with Constraints (Edits)

Lock specific fields to exact values before generation. The AI will generate the remaining fields while honoring your constraints.

edits.ts
// Generate a recipe, but portions MUST be 4
const { proposal } = await pf.generate('recipe', {
  input: 'A hearty pasta dinner',
  edits: { portions: 4 },  // Constraint: portions will be exactly 4
});

console.log(proposal.generatedObject);
// {
//   title: "Creamy Pasta Carbonara",
//   ingredients: [...scaled for 4 people],
//   portions: 4  // Guaranteed to be 4
// }

Model Tier Selection

Choose the AI model quality/speed tradeoff for each generation. Model tiers abstract away specific model versions, letting you optimize for speed, quality, or cost.

model-tier.ts
// Use the fastest model for quick suggestions
const { proposal } = await pf.generate('recipe', {
  input: 'A quick pasta dish',
  modelTier: 'fast',  // Optimized for speed and cost
});

// Use the highest quality model for important content
const { proposal: premium } = await pf.generate('blogPost', {
  input: 'Write a detailed technical analysis',
  modelTier: 'quality',  // Highest quality output
});

// Available model tiers:
// - 'fast': Optimized for speed and cost (e.g., Claude Haiku)
// - 'balanced': Good balance of quality and cost (e.g., Claude Sonnet) - default
// - 'quality': Highest quality output (e.g., Claude Opus)

// Model tier can also be set at schema or application level
// Priority: per-generation > schema > application > system default

Each model tier has a different credit cost. See the configuration docs for credit rates.

Generation Options

options.ts
interface GenerateInput {
  // Required: Natural language input
  input: string;

  // Optional: Fields that must have exact values (constraints)
  edits?: Record<string, unknown>;

  // Optional: Metadata to attach to the proposal
  metadata?: Record<string, unknown>;

  // Optional: Subject attribution for user analytics
  subject?: { userId?: string; tenantId?: string; [key: string]: unknown };

  // Optional: Generate AI description and justification
  generateSuggestionMeta?: boolean;

  // Optional: Generate 3-5 suggested feedback phrases
  generateSuggestedResponses?: boolean;

  // Optional: 'llm' (default) or 'mock' for testing without LLM costs
  generationMode?: 'llm' | 'mock';

  // Optional: Explicit expiration timestamp (defaults to 7 days)
  expiresAt?: string | Date;

  // Optional: Expiration duration in milliseconds
  expirationDurationMs?: number;

  // Optional: Model tier for quality/speed tradeoff
  // 'fast' | 'balanced' (default) | 'quality'
  modelTier?: ModelTier;
}

// Response structure
interface GenerateResponse<T> {
  proposal: Proposal<T>;
  generation: {
    id: string;
    model: string;
    modelTier: ModelTier;  // The tier used
    promptTokens: number;
    completionTokens: number;
    credits: number;  // Credits consumed
    latencyMs: number;
    edits?: Record<string, unknown>;  // Constraints used
  };
  suggestionMeta?: {
    description: string;
    justification: string;
  };
  suggestedResponses?: string[];
}

Regenerating with Feedback

Use regenerate() to create a new proposal based on user feedback.

regenerate.ts
// Original proposal wasn't quite right
const { proposal: original } = await pf.generate('blogPost', {
  input: 'Write about TypeScript',
});

// Regenerate with feedback
const { proposal: improved } = await pf.proposals.regenerate(original.id, {
  feedback: 'Make it more beginner-friendly and add code examples',
});

// New proposal is linked to the original
console.log(improved.parentProposalId);  // original.id

Reject with Edits (Constrained Regeneration)

When rejecting a proposal, you can include edits that become constraints for the next regeneration. This is the core iterative refinement loop.

reject-with-edits.ts
// 1. Generate a recipe
const { proposal } = await pf.generate('recipe', {
  input: 'A pasta dish for dinner',
});
// → { title: 'Tomato Pasta', ingredients: [...], portions: 2 }

// 2. User wants 4 portions - reject with edits
await pf.proposals.decide(proposal.id, {
  action: 'reject',
  reason: 'Need more portions for the family',
  edits: { portions: 4 },  // This becomes a constraint
});

// 3. Regenerate - edits are automatically used as constraints
const { proposal: improved } = await pf.proposals.regenerate(proposal.id, {});
// → { title: 'Tomato Pasta', ingredients: [...scaled up], portions: 4 }
// portions is guaranteed to be 4, ingredients are scaled accordingly

// 4. User approves the improved version
await pf.proposals.decide(improved.id, { action: 'approve' });

This workflow allows users to iteratively refine proposals by locking in the parts they like while asking the AI to regenerate the rest.

Batch Generation

Generate multiple proposals in parallel.

batch.ts
const topics = [
  'Introduction to Docker',
  'Kubernetes for Beginners',
  'CI/CD Best Practices',
];

const results = await Promise.all(
  topics.map((topic) =>
    pf.generate('blogPost', { input: `Write a blog post about ${topic}` })
  )
);

// results is typed as GenerateResponse<BlogPost>[]
for (const { proposal } of results) {
  console.log(proposal.generatedObject.title);
}

Mock Generation for Testing

Use generationMode: 'mock' to generate schema-valid random data without calling the LLM. This is useful for testing your approval workflows without incurring LLM costs.

mock-generation.ts
// Generate mock data for testing (no LLM cost)
const { proposal, generation } = await pf.generate('blogPost', {
  input: 'Write about TypeScript',
  generationMode: 'mock',
});

// Mock proposals have model: 'mock'
console.log(generation.model);  // 'mock'

// Generated object contains schema-valid placeholder data
console.log(proposal.generatedObject);
// {
//   title: "Mock string value",
//   content: "Mock string value",
//   tags: ["Mock string value"]
// }

// Webhooks still fire, enabling end-to-end workflow testing

Error Handling

errors.ts
import { ProposeFlowError, RateLimitError } from '@proposeflow/sdk';

try {
  const { proposal } = await pf.generate('blogPost', {
    input: 'Write a blog post',
  });
} catch (error) {
  if (error instanceof RateLimitError) {
    // Wait and retry
    console.log(`Rate limited. Retry after ${error.retryAfter}ms`);
  } else if (error instanceof ProposeFlowError) {
    // Handle API errors
    console.error(`API Error: ${error.message}`, error.code);
  }
}