Private BetaProposeFlow is currently in private beta.Join the waitlist

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.

Proposal structure
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.

generate.ts
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 ms

Generation Info

Each proposal includes generation metadata with model tier and credit information.

Generation structure
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>;
}
Accessing generation info
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);  // 2340

See 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.

fetch.ts
// 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

1

Pending

Newly created proposals start in pending status, awaiting user review.

2a

Approved

User approved the proposal (with optional edits). The final data is safe to persist.

2b

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.

2c

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.

Custom expiration
// 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.

Expiration filters
// 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');