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.
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); // 1523User Attribution (Subject)
Use the subject field to attribute proposals to specific users. This enables user activity analytics on the dashboard.
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.
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.
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 proposalSuggested 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".
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.
// 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.
// 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 defaultEach model tier has a different credit cost. See the configuration docs for credit rates.
Generation Options
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.
// 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.idReject 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.
// 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.
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.
// 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 testingError Handling
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);
}
}