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.
// 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
// 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.
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.
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
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.
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.
| Event | Description |
|---|---|
event.completed | Event processed (may have proposals or be skipped) |
event.failed | Event processing failed |
proposals.generated | One or more proposals were generated |
Webhook Payload: proposals.generated
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
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
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 };
}