Private BetaProposeFlow is currently in private beta.Join the waitlist

Event-Driven Generation

Register domain events from your application and let AI automatically determine when to generate proposals. You can send relationship data directly or register a resolver for automatic fetching.

Overview

Event-driven generation enables reactive AI workflows. Instead of manually calling generate(), you emit events when objects change in your system, and ProposeFlow automatically determines if related objects should be updated.

Example use case: When a user comments "This recipe needs more baking time", ProposeFlow can automatically propose an updated recipe with adjusted cooking times.

Registering Events

Use registerEvent to emit an event with the triggering object and related data. The API returns immediately with an event ID - results are delivered via webhooks. If relationship data is trimmed or missing required identifiers, the response includes warnings.

You can also attach optional metadata, like forceAnalyze, to bypass server-side filters or gating for critical events.

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.id,
    data: {
      text: comment.text,
      recipeId: recipe.id,
      createdAt: comment.createdAt,
    },
  },
  subject: { userId: session.user.id },
  metadata: { forceAnalyze: false },
  relationshipMode: 'resolve',
});

console.log('Event registered:', eventId);
if (warnings?.length) console.warn('Event warnings:', warnings);
// Results delivered via 'proposals.generated' webhook

Event Types

Event TypeDescription
object_createdA new object was created (e.g., comment, review, feedback)
object_updatedAn existing object was modified
object_deletedAn object was deleted

Full Type Safety

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

typed-events.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,
  },
});

// 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: [existingRecipe],     // ✓ Must be Recipe[] (include a stable id)
    // invalid: [data],           // ✗ TypeScript error - not a schema
  },
});

How Analysis Works

When an event is registered, the LLM analyzes the event and relationships to determine if any proposals should be generated. This happens asynchronously.

1

Event received

API validates the event and returns an event ID immediately (202 Accepted)

2

LLM analysis

The pipeline can debounce and coalesce related events, run deterministic pre-filters, and optionally pass through a lightweight gate before full analysis.

3

Proposal generation

If changes are warranted, the AI generates a proposal with the updated object and explanation

4

Webhook delivery

Results are delivered via webhooks: proposals.generated, event.completed, or event.failed

Invocation Controls

ProposeFlow can reduce LLM calls with deterministic pre-filters, a lightweight gate, per-object coalescing, decision caching, and tiered models. When a filter or gate skips an event, the skip reason is recorded on the event and surfaced in webhooks. Cache hits are also recorded for auditability.

skip-reason.ts
// event.analysisResult includes skipReason for transparency
if (event.analysisResult?.skipReason) {
  console.log(event.analysisResult.skipReason.type);
  // 'prefilter' | 'gate' | 'coalesce' | 'classification'
}

if (event.analysisResult?.cache?.hit) {
  console.log('Cache key:', event.analysisResult.cache.cacheKey);
}

// coalesced events include a summary of merged IDs
if (event.analysisResult?.coalesced) {
  console.log(event.analysisResult.coalesced.eventIds);
}

Classification

Classification enables LLM-based subjective filtering of events. Define criteria like "is constructive", "provides reasoning", or "is critical issue" and the LLM will evaluate each event against them before deciding whether to proceed with full analysis.

Dashboard UI: Classification is configured per relationship resolver. Navigate to Relationship Resolvers and select a resolver to configure its classification criteria. The UI includes a test panel to preview how events will be classified.

When to use classification: Use classification when you need subjective judgments that can't be expressed as simple rules. For example, filtering out "thanks!" comments that don't warrant recipe changes, or prioritizing feedback that explains reasoning.

relationship-with-classification.ts
// Classification is configured as part of the relationship resolver
await pf.relationships.register({
  sourceType: 'comment',
  relationshipName: 'recipe',
  url: 'https://api.example.com/proposeflow/relationships',
  classificationConfig: {
    enabled: true,
    criteria: [
      {
        name: 'constructive',
        description: 'Provides actionable feedback or suggestions rather than vague complaints',
        requiredValue: true
      },
      {
        name: 'provides_reasoning',
        description: 'Explains WHY something is good/bad or provides context for the feedback',
        requiredValue: true
      }
    ],
    combineMode: 'all'  // All criteria must be true
  }
});

Each criterion has a description (used as instructions for the LLM) and optional requiredValue to specify whether the criterion must be true or false. All criteria must pass for an event to proceed.

Classification results are stored on the event and can be used to filter events in the API:

filter-by-classification.ts
// Fetch only events classified as constructive
const response = await fetch(
  '/v1/events?classification.constructive=true',
  { headers: { Authorization: `Bearer ${apiKey}` } }
);

// Access classification results on individual events
const event = await pf.events.get(eventId);
if (event.classificationResult) {
  console.log('Classification score:', event.classificationResult.score);
  console.log('Should proceed:', event.classificationResult.shouldProceed);

  for (const [name, result] of Object.entries(event.classificationResult.criteria)) {
    console.log(`${name}: ${result.value} (${result.reasoning})`);
  }
}

Handling Event Webhooks

Subscribe to event webhooks to receive analysis results and generated proposals.

webhook-handler.ts
// Create webhook endpoint for event results
await pf.webhooks.create({
  url: 'https://your-app.com/api/webhooks/proposeflow',
  events: [
    'event.completed',       // Event processed (may or may not have proposals)
    'event.failed',          // Event processing failed
    'proposals.generated',   // Proposals were generated
  ],
  secret: 'whsec_your_secret',
});
app/api/webhooks/proposeflow/route.ts
export async function POST(req: NextRequest) {
  const event = await parseAndVerifyWebhook(req);

  switch (event.type) {
    case 'proposals.generated':
      // Handle generated proposals
      for (const proposal of event.data.proposals) {
        console.log('New proposal:', proposal.id);
        console.log('Description:', proposal.suggestionMeta?.description);
        console.log('Generated object:', proposal.generatedObject);

        // Show to user for approval, or auto-approve based on rules
        await showProposalToUser(proposal);
      }
      break;

    case 'event.completed':
      console.log('Event processed:', event.data.eventId);
      console.log('Status:', event.data.status);  // 'completed' or 'skipped'
      console.log('Proposal count:', event.data.proposalCount);
      if (event.data.skipReason) {
        console.log('Skip reason:', event.data.skipReason.type);
      }
      if (event.data.cacheHit) {
        console.log('Cache hit:', event.data.cacheKey);
      }
      if (event.data.coalescedEventIds) {
        console.log('Coalesced event IDs:', event.data.coalescedEventIds);
      }
      break;

    case 'event.failed':
      console.error('Event failed:', event.data.eventId, event.data.error);
      break;
  }

  return NextResponse.json({ received: true });
}

Proposal Actions

The AI determines what action to take based on the event context. Generated proposals include a proposalAction field indicating the intended operation.

ActionDescription
updateModify an existing object in the relationship array
createCreate a new object of the target schema type
deleteMark an existing object for deletion

Suggestion Metadata

Generated proposals include AI-generated explanations to help users understand the suggested changes.

suggestion-meta.ts
// Each proposal includes suggestionMeta with:
interface SuggestionMeta {
  description: string;    // "Increased bake time to 45 minutes"
  justification: string;  // "User requested longer baking for better results"
}

// Use in your UI
const proposal = event.data.proposals[0];
console.log(proposal.suggestionMeta?.description);
// "Increased bake time to 45 minutes"
console.log(proposal.suggestionMeta?.justification);
// "User requested longer baking for better results"

Fetching Event Status

You can fetch an event's status and results at any time using the event ID.

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

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

if (event.status === 'completed' && event.proposals) {
  console.log('Generated proposals:', event.proposals.length);
}

// Classification results (if classification is enabled)
if (event.classificationResult) {
  console.log('Classification score:', event.classificationResult.score);
  console.log('Should proceed:', event.classificationResult.shouldProceed);
}

if (event.analysisResult) {
  console.log('Should generate:', event.analysisResult.shouldGenerate);
  console.log('Reasoning:', event.analysisResult.reasoningText ?? event.analysisResult.reasoning);
  if (event.analysisResult.cache?.hit) {
    console.log('Cache key:', event.analysisResult.cache.cacheKey);
  }
}

Complete Example

Here's a complete example of event-driven generation in a Next.js application.

app/api/recipes/[id]/comments/route.ts
import { pf } from '@/lib/proposeflow';
import { db } from '@/lib/db';

export async function POST(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const { text } = await req.json();
  const recipeId = params.id;

  // Save the comment
  const comment = await db.comment.create({
    data: { text, recipeId },
  });

  // Get the recipe data for context
  const recipe = await db.recipe.findUnique({
    where: { id: recipeId },
  });

  // Register event - fully type-safe with schema registry
  const { eventId } = await pf.registerEvent('object_created', {
    object: {
      type: 'comment',
      id: comment.id,
      data: {
        text,
        recipeId,
        createdAt: comment.createdAt.toISOString(),
      },
    },
    relationships: {
      recipe: [recipe],  // Type-checked as Recipe[]
    },
  });

  return NextResponse.json({
    comment,
    eventId,  // Client can poll or wait for webhook
  });
}

Best Practices

  • Include relevant context - The more relationship data you provide, the better the AI can understand what changes are needed
  • Use specific object types - Name your object types clearly (e.g., "comment", "review", "feedback") to help the AI understand the context
  • Handle webhooks idempotently - Webhooks may be retried, so ensure your handler can safely process the same event multiple times
  • Show suggestions to users - Use the suggestionMeta to display clear explanations of what the AI is proposing and why