Private BetaProposeFlow is currently in private beta.Join the waitlist

Events API

Register domain events and let AI automatically determine when to generate proposals based on object changes in your application.

registerEvent()

Register an event for asynchronous AI analysis. Returns immediately with an event ID - results are delivered via webhooks.

Attach optional metadata (for example, forceAnalyze) to bypass server-side filters and gating when needed.

register-event.ts
// One-time setup: register a relationship resolver
await pf.relationships.register({
  sourceType: 'comment',
  relationshipName: 'recipe',
  url: 'https://api.example.com/proposeflow/relationships',
  supportsBatch: true,
  presets: [
    {
      name: 'default',
      description: 'Fetch the recipe tied to the comment',
      isDefault: true,
    },
    {
      name: 'byId',
      description: 'Lookup by recipeId from the event payload',
      params: ['recipeId'],
    },
  ],
  queryRules: {
    filters: [
      { field: 'id', operators: ['eq', 'in'] },
      { field: 'tags', operators: ['contains'] },
    ],
    sorts: ['createdAt'],
    select: ['id', 'title', 'tags', 'createdAt'],
    maxLimit: 20,
  },
});

const { eventId, warnings } = await pf.registerEvent('object_created', {
  object: {
    type: 'comment',
    id: 'comment_123',
    data: {
      text: 'This needs more salt',
      recipeId: 'recipe_456',
      createdAt: new Date().toISOString(),
    },
  },
  subject: { userId: 'user_123' },
  metadata: { forceAnalyze: false },
  relationshipMode: 'resolve',
});

console.log('Event registered:', eventId);
if (warnings?.length) console.warn('Event warnings:', warnings);
// 'pf_evt_abc123'

RegisterEventInput

types.ts
// Discriminated union - type constrains data
type EventObject<T extends SchemaRegistry> = {
  [K in keyof T & string]: {
    type: K;                  // Must be a registered schema name
    id?: string;              // Optional identifier
    data: z.infer<T[K]>;      // Typed by the schema
  };
}[keyof T & string];

interface RegisterEventInput<T extends SchemaRegistry> {
  // The type of event
  eventType: 'object_created' | 'object_updated' | 'object_deleted';

  // The object that triggered the event (type-safe)
  object: EventObject<T>;

  // Related objects to analyze (keys and values typed by schema)
  relationships?: Partial<{
    [K in keyof T & string]: (z.infer<T[K]> & { id?: string })[];
  }>;

  // How relationship data is provided
  relationshipMode?: 'manual' | 'resolve';

  // End-user context for auditing
  subject?: Record<string, unknown>;

  // Optional metadata
  metadata?: Record<string, unknown>;
}

Full Type Safety

Both the event object and relationships are fully type-checked against your schema registry. The type field acts as a discriminator that constrains the data type.

typed-events.ts
const pf = new ProposeFlow({
  apiKey: process.env.PROPOSEFLOW_API_KEY!,
  schemas: {
    recipe: RecipeSchema,
    comment: CommentSchema,
  },
});

// TypeScript enforces valid schema names AND data types
await pf.registerEvent('object_created', {
  object: {
    type: 'comment',              // ✓ Must be 'recipe' | 'comment'
    data: {
      text: 'Needs more salt',    // ✓ Typed as Comment
      recipeId: 'recipe_123',
      createdAt: new Date().toISOString(),
    },
  },
  relationshipMode: 'manual',
  relationships: {
    recipe: [recipe],             // ✓ Must be Recipe[] (include a stable id)
    // invalid: [data],           // ✗ TypeScript error - not a schema
  },
});

// Wrong data shape is caught at compile time
await pf.registerEvent('object_created', {
  object: {
    type: 'recipe',
    data: { text: 'wrong' },      // ✗ Error: Recipe requires title, steps, etc.
  },
  relationships: {},
});

events.get()

Fetch an event by ID to check its status and results.

get-event.ts
const event = await pf.events.get('pf_evt_abc123');

console.log(event.status);
// 'pending' | 'completed' | 'skipped' | 'failed'

console.log(event.eventType);       // 'object_created'
console.log(event.objectType);      // 'comment'
console.log(event.objectData);      // { text: '...', recipeId: '...' }
console.log(event.relationships);   // { recipe: [...] }

// Analysis result (if processed)
if (event.analysisResult) {
  console.log(event.analysisResult.shouldGenerate);  // true
  console.log(event.analysisResult.reasoningText ?? event.analysisResult.reasoning); // 'User suggested...'
  console.log(event.analysisResult.action);          // 'update'
  console.log(event.analysisResult.targetRelationship); // 'recipe'
  if (event.analysisResult.cache?.hit) {
    console.log('Cache key:', event.analysisResult.cache.cacheKey);
  }
}

// Generated proposals
if (event.proposals) {
  for (const proposal of event.proposals) {
    console.log(proposal.id, proposal.generatedObject);
  }
}

Event Interface

event-type.ts
interface Event<T extends SchemaRegistry> {
  id: string;
  eventType: 'object_created' | 'object_updated' | 'object_deleted';
  objectType: string;
  objectData: unknown;
  relationships: {
    [K in keyof T & string]?: (z.infer<T[K]> & { id?: string })[];
  };
  relationshipMode?: 'manual' | 'resolve';
  subject?: Record<string, unknown> | null;
  metadata?: Record<string, unknown> | null;
  ingestWarnings?: {
    relationship: string;
    code: string;
    message: string;
  }[];
  relationshipQueries?: {
    relationship: string;
    preset?: string;
    params?: Record<string, unknown>;
    limit?: number;
    query?: {
      filters?: { field: string; op: string; value: unknown }[];
      sort?: { field: string; direction?: 'asc' | 'desc' }[];
      select?: string[];
    };
    source?: 'default' | 'llm';
  }[];
  analysisResult?: {
    shouldGenerate: boolean;
    reasoning?: string;
    reasoningText?: string;
    analysisModel?: string;
    generationModel?: string | null;
    action?: 'update' | 'delete' | 'create';
    targetRelationship?: keyof T & string;
    targetObjectId?: string;
    cache?: {
      hit: boolean;
      cacheKey: string;
      entryId?: string;
      cachedAt?: string;
      expiresAt?: string | null;
    };
    skipReason?: {
      type: 'prefilter' | 'gate' | 'coalesce';
      [key: string]: unknown;
    };
    coalesced?: {
      eventIds: string[];
      eventTypes: string[];
      count: number;
      firstEventAt: string;
      lastEventAt: string;
      windowMs: number;
      forceAnalyze: boolean;
    };
  };
  status: 'pending' | 'completed' | 'skipped' | 'failed';
  createdAt: string;
  proposals?: Proposal<T>[];
}

Invocation Controls

ProposeFlow can reduce LLM usage with pre-filters, a gate model, coalescing, decision caching, and tiered analysis/generation models. Cache hits and model choices are recorded in the event analysis payload for auditing.

analysis-controls.ts
if (event.analysisResult) {
  console.log(event.analysisResult.analysisModel);
  console.log(event.analysisResult.generationModel);
  if (event.analysisResult.cache?.hit) {
    console.log('Cache entry:', event.analysisResult.cache.entryId);
  }
}

Webhook Events

Subscribe to these events to receive results asynchronously.

EventDescription
event.completedEvent processed (may have proposals or be skipped)
event.failedEvent processing failed
proposals.generatedOne or more proposals were generated

Webhook Payload: proposals.generated

webhook-payload.ts
interface ProposalsGeneratedPayload {
  eventId: string;
  proposals: Array<{
    id: string;
    schemaId: string;
    schemaName: string;
    status: 'pending';
    proposalAction: 'create' | 'update' | 'delete';
    generatedObject: Record<string, unknown>;
    suggestionMeta: {
      description: string;    // "Added more salt to the recipe"
      justification: string;  // "User mentioned needing more salt"
    } | null;
    metadata: {
      eventType: string;
      objectType: string;
      objectId?: string;
      objectData: unknown;
    };
    createdAt: string;
  }>;
}

Webhook Payload: event.completed

event-completed.ts
interface EventCompletedPayload {
  eventId: string;
  status: 'completed' | 'skipped';
  proposalCount: number;
  skipReason?: {
    type: 'prefilter' | 'gate' | 'coalesce';
    [key: string]: unknown;
  };
  cacheHit?: boolean;
  cacheKey?: string;
  coalescedEventIds?: string[];
}

Complete Example

complete-example.ts
import { ProposeFlow } from '@proposeflow/sdk';
import { z } from 'zod';

const RecipeSchema = z.object({
  title: z.string(),
  cookTime: z.number(),
  ingredients: z.array(z.object({
    item: z.string(),
    amount: z.string(),
  })),
});

const CommentSchema = z.object({
  text: z.string(),
  recipeId: z.string(),
  createdAt: z.string(),
});

const pf = new ProposeFlow({
  apiKey: process.env.PROPOSEFLOW_API_KEY!,
  schemas: {
    recipe: RecipeSchema,
    comment: CommentSchema,
  },
});

// Set up webhook for results
await pf.webhooks.create({
  url: 'https://your-app.com/webhooks/proposeflow',
  events: ['proposals.generated', 'event.completed', 'event.failed'],
  secret: 'whsec_your_secret',
});

// Register an event when a user comments
async function handleComment(text: string, recipeId: string) {
  const recipe = await db.recipe.findUnique({
    where: { id: recipeId },
  });

  // Fully type-safe: object.type and object.data are validated
  const { eventId } = await pf.registerEvent('object_created', {
    object: {
      type: 'comment',
      data: {
        text,
        recipeId,
        createdAt: new Date().toISOString(),
      },
    },
    relationships: {
      recipe: [recipe],  // Type-checked as Recipe[]
    },
  });

  // Return immediately - proposals delivered via webhook
  return { eventId };
}