Proposals
Proposals are AI-generated objects awaiting human review. They contain the generated data, metadata, and status information.
What is a Proposal?
When you call generate(), ProposeFlow creates a proposal — a pending object that needs human approval before it becomes permanent. This is the core of the human-in-the-loop workflow.
interface Proposal<T> {
id: string; // Unique proposal ID
schemaId: string; // Reference to the schema used
schemaName: string; // Schema name for type discrimination
status: 'pending' | 'approved' | 'rejected' | 'regenerating';
generatedObject: T; // The generated data (typed!)
generationId: string; // Reference to the generation
parentProposalId: string | null; // Links to previous proposal if regenerated
metadata: Record<string, unknown>;
createdAt: string;
expiresAt?: string | null; // When the proposal expires (ISO 8601)
decision?: Decision | null; // Decision details if one was made
}Generating Proposals
Generate a proposal by calling the generate() method on a registered schema.
const { proposal, generation } = await pf.generate('blogPost', {
input: 'Write a blog post about the benefits of TypeScript',
metadata: {
audience: 'intermediate developers',
tone: 'professional but friendly',
},
});
console.log(proposal.id); // 'prop_abc123...'
console.log(proposal.status); // 'pending'
console.log(proposal.generatedObject); // { title: '...', summary: '...', ... }
console.log(generation.latencyMs); // Generation time in msGeneration Info
Each proposal includes generation metadata with model tier and credit information.
interface Generation {
id: string; // Unique generation ID
model: string; // Model identifier used
modelTier: 'fast' | 'balanced' | 'quality';
promptTokens: number; // Input tokens consumed
completionTokens: number; // Output tokens generated
credits: number; // Credits consumed for this generation
latencyMs: number; // Generation time in milliseconds
edits?: Record<string, unknown>;
}const { proposal, generation } = await pf.generate('blogPost', {
input: 'Write a post about AI safety',
modelTier: 'quality',
});
// Track costs per generation
console.log(generation.modelTier); // 'quality'
console.log(generation.credits); // 15.5
console.log(generation.latencyMs); // 2340See Model Tiers & Credits for details on choosing the right tier and understanding credit calculation.
Fetching Proposals
Retrieve a proposal by its ID, or list proposals with filters.
// Get a single proposal (returns typed discriminated union)
const proposal = await pf.proposals.get('prop_abc123');
// List proposals with filters
const { data: proposals, nextCursor } = await pf.proposals.list({
status: 'pending',
schemaId: 'sch_xyz789',
limit: 10,
});
// Iterate with type-safe access via schemaName discriminator
for (const proposal of proposals) {
if (proposal.schemaName === 'blogPost') {
console.log(proposal.generatedObject.title); // Fully typed!
}
}Proposal Lifecycle
Pending
Newly created proposals start in pending status, awaiting user review.
Approved
User approved the proposal (with optional edits). The final data is safe to persist.
Regenerating
User rejected with feedback. A new proposal is being generated that incorporates their input. The new proposal links back to this one via parentProposalId.
Rejected
User rejected without requesting regeneration. This is a terminal state, useful for tracking rejection metrics and preventing the proposal from being shown again.
Iterative refinement: The typical flow is Pending → Regenerating → Pending → ... → Approved. Each regeneration creates a new proposal that builds on the user's feedback, continuing until they approve.
Expiration
Proposals automatically expire after 7 days by default if no decision is made. You can customize this duration when generating proposals.
// Set a specific expiration date
const { proposal } = await pf.generate('task', {
input: 'Review Q4 report',
expiresAt: new Date('2025-02-01'),
});
// Or set a custom duration (1 day = 86400000 ms)
const { proposal } = await pf.generate('task', {
input: 'Urgent review needed',
expirationDurationMs: 24 * 60 * 60 * 1000, // 1 day
});Querying Expired Proposals
After a proposal expires, it won't be returned when listing proposals. However, expired proposals are still available for reporting or if queried explicitly.
// List returns only non-expired proposals by default
const { data: proposals } = await pf.proposals.list();
// Explicitly include expired proposals (for reporting)
const { data: expired } = await pf.proposals.list({ expired: true });
// Get a specific proposal by ID (works even if expired)
const proposal = await pf.proposals.get('prop_abc123');