You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The AI Trend Search Engine is a new module within the Marketing Engine service that accepts a natural language prompt and company context, searches the web for recent news and events, and returns 5 structured results with citations. It runs as an Inngest background job and is designed as a self-contained module that can later be invoked by AI agents.
Persistence is handled by the caller (Inngest job), not the core pipeline. This keeps the module stateless and reusable.
The module lives under src/server/trend-search/ as a standalone directory, with an Inngest function in src/server/inngest/functions/ and an API route in src/app/api/trend-search/.
Architecture
sequenceDiagram
participant Client
participant API as API Route<br/>/api/trend-search
participant Inngest as Inngest Job
participant QP as Query Planner<br/>(LLM)
participant WS as Web Search<br/>(Tavily)
participant CS as Content Synthesizer<br/>(LLM)
participant DB as PostgreSQL
Client->>API: POST { query, companyContext, categories? }
API->>API: Validate input (Zod)
API->>Inngest: Dispatch "trend-search/run.requested"
API-->>Client: 202 { jobId, status: "queued" }
Inngest->>QP: Step 1: Plan queries
QP-->>Inngest: sub-queries[]
loop For each sub-query
Inngest->>WS: Step 2: Execute search
WS-->>Inngest: raw results[]
end
Inngest->>CS: Step 3: Synthesize results
CS-->>Inngest: SearchResult[5]
Inngest->>DB: Step 4: Persist results
DB-->>Inngest: saved
Note over Client,DB: Client polls GET /api/trend-search/[jobId] for results
Loading
The module integrates with the existing architecture:
Services Layer: Lives within the Marketing Engine boundary
Tools Layer: Uses Web Search (Tavily) and LLM capabilities (OpenAI via LangChain)
Physical Layer: Persists to PostgreSQL, runs via Inngest
This function runs the pipeline directly (without Inngest, without DB) for synchronous invocation by agents or any caller. It accepts only the search input and returns the result — no side effects. Persistence is the responsibility of the caller.
The Inngest function is the only caller that persists results to the DB. This keeps runTrendSearch() stateless and reusable across contexts (agents, tests, scripts) without requiring DB access.
A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: Valid input creates a job
For any valid Search_Query (non-empty, ≤1000 chars) and valid Company_Context (non-empty, ≤2000 chars), submitting a trend search should create a job record with status "queued" and return a job ID.
Validates: Requirements 1.1
Property 2: Invalid input is rejected
For any Search_Query composed entirely of whitespace characters, or for any Company_Context exceeding 2000 characters, the Trend_Search_Engine should reject the request with a validation error and not create a job record.
For any valid Search_Query and Company_Context where no Search_Categories are specified, the Query_Planner should return planned queries where every category is a member of the valid SearchCategory enum (fashion, finance, business, tech).
Validates: Requirements 1.2
Property 4: Specified categories are preserved in planned queries
For any set of specified Search_Categories, all planned queries produced by the Query_Planner should only reference categories from the specified set.
For any valid Search_Query and Company_Context, the Query_Planner should return at least one PlannedQuery.
Validates: Requirements 2.1
Property 6: Every sub-query triggers a search call
For any list of PlannedQueries, the web search executor should invoke the search provider exactly once per sub-query (before retries).
Validates: Requirements 3.1
Property 7: Synthesizer output structure
For any set of at least 5 raw search results, the Content_Synthesizer should produce exactly 5 SearchResult objects, each containing a non-empty sourceUrl, a non-empty summary, and a non-empty description.
Validates: Requirements 4.1, 4.2
Property 8: Source URL traceability
For any output from the Content_Synthesizer, every sourceUrl in the results should be present in the set of URLs from the raw input results.
Validates: Requirements 4.3
Property 9: Persistence round-trip
For any completed trend search, persisting the Result_Set and then retrieving it by job ID should return an equivalent Result_Set, including the original query, company context, categories, and a timestamp.
Validates: Requirements 5.1, 5.2, 5.3
Property 10: Company data isolation
For any two distinct company IDs, trend search results persisted under company A should never appear when querying results for company B.
Validates: Requirements 5.4
Property 11: Successful pipeline sets completed status
For any trend search pipeline that completes all steps without error, the job record status should be "completed" and the completedAt timestamp should be non-null.
Validates: Requirements 6.4
Property 12: Input serialization round-trip
For any valid TrendSearchInput, serializing it to the Inngest event JSON payload and then deserializing it back should produce an object equal to the original input.
Validates: Requirements 8.1, 8.2
Error Handling
Error Scenario
Handling Strategy
Empty/whitespace query
Zod validation rejects at API layer, returns 400 with error details
Company context too long
Zod validation rejects at API layer, returns 400 with error details
Tavily API unavailable
Retry up to 2 times per sub-query (Inngest step retry). Log and continue with remaining sub-queries
Tavily returns 0 results for a sub-query
Log warning, continue with other sub-queries
All sub-queries return 0 results
Synthesizer returns 5 placeholder results with empty sourceUrl and "Insufficient results" summary
LLM (Query Planner) fails
Inngest step retry (up to 3 retries). If all fail, mark job as failed
LLM (Synthesizer) fails
Inngest step retry (up to 3 retries). If all fail, mark job as failed
LLM returns malformed output
Zod validation on LLM output catches structural issues. Retry the step
DB write fails
Inngest step retry. If persistent, mark job as failed with error message
Unauthorized request
Clerk auth middleware returns 401 before reaching the handler
Job not found on poll
Return 404
Job belongs to different company
Return 404 (do not leak existence)
Testing Strategy
Property-Based Testing
Use fast-check as the property-based testing library (already compatible with the Jest setup in this project).
Each correctness property maps to a single property-based test with a minimum of 100 iterations. Tests should be tagged with the property reference.
Tag format: Feature: ai-trend-search-engine, Property {N}: {title}
Key property tests:
Input validation properties (P1, P2): Generate random valid/invalid inputs and verify acceptance/rejection
Design Document: AI Trend Search Engine
Overview
The AI Trend Search Engine is a new module within the Marketing Engine service that accepts a natural language prompt and company context, searches the web for recent news and events, and returns 5 structured results with citations. It runs as an Inngest background job and is designed as a self-contained module that can later be invoked by AI agents.
The pipeline follows four stages:
Persistence is handled by the caller (Inngest job), not the core pipeline. This keeps the module stateless and reusable.
The module lives under
src/server/trend-search/as a standalone directory, with an Inngest function insrc/server/inngest/functions/and an API route insrc/app/api/trend-search/.Architecture
sequenceDiagram participant Client participant API as API Route<br/>/api/trend-search participant Inngest as Inngest Job participant QP as Query Planner<br/>(LLM) participant WS as Web Search<br/>(Tavily) participant CS as Content Synthesizer<br/>(LLM) participant DB as PostgreSQL Client->>API: POST { query, companyContext, categories? } API->>API: Validate input (Zod) API->>Inngest: Dispatch "trend-search/run.requested" API-->>Client: 202 { jobId, status: "queued" } Inngest->>QP: Step 1: Plan queries QP-->>Inngest: sub-queries[] loop For each sub-query Inngest->>WS: Step 2: Execute search WS-->>Inngest: raw results[] end Inngest->>CS: Step 3: Synthesize results CS-->>Inngest: SearchResult[5] Inngest->>DB: Step 4: Persist results DB-->>Inngest: saved Note over Client,DB: Client polls GET /api/trend-search/[jobId] for resultsThe module integrates with the existing architecture:
Components and Interfaces
1. Input Types (
src/server/trend-search/types.ts)2. Query Planner (
src/server/trend-search/query-planner.ts)Responsible for taking the user's prompt and company context and generating optimized sub-queries for Tavily.
Implementation approach:
ChatOpenAI) with a structured output prompt3. Web Search Executor (
src/server/trend-search/web-search.ts)Executes sub-queries against Tavily and collects raw results.
Implementation approach:
@langchain/communityTavilySearchResults tool or direct Tavily APIsearch_depth: "advanced"andtopic: "news"to focus on news/events4. Content Synthesizer (
src/server/trend-search/synthesizer.ts)Takes raw search results and produces exactly 5 structured results.
Implementation approach:
sourceUrl: "",summary: "Insufficient results")5. Inngest Function (
src/server/inngest/functions/trendSearch.ts)Orchestrates the pipeline as a multi-step Inngest function.
6. API Routes
POST
/api/trend-search— Initiate a search202 { jobId, status: "queued" }GET
/api/trend-search/[jobId]— Poll for resultsGET
/api/trend-search— List past searches7. Module Entry Point (
src/server/trend-search/index.ts)Exposes the public interface for programmatic invocation (agent-callable in the future).
This function runs the pipeline directly (without Inngest, without DB) for synchronous invocation by agents or any caller. It accepts only the search input and returns the result — no side effects. Persistence is the responsibility of the caller.
The Inngest function is the only caller that persists results to the DB. This keeps
runTrendSearch()stateless and reusable across contexts (agents, tests, scripts) without requiring DB access.Data Models
New Table:
trend_search_jobsDrizzle schema definition in
src/server/db/schema/trend-search.ts:Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: Valid input creates a job
For any valid Search_Query (non-empty, ≤1000 chars) and valid Company_Context (non-empty, ≤2000 chars), submitting a trend search should create a job record with status "queued" and return a job ID.
Validates: Requirements 1.1
Property 2: Invalid input is rejected
For any Search_Query composed entirely of whitespace characters, or for any Company_Context exceeding 2000 characters, the Trend_Search_Engine should reject the request with a validation error and not create a job record.
Validates: Requirements 1.4, 1.5
Property 3: Category inference produces valid categories
For any valid Search_Query and Company_Context where no Search_Categories are specified, the Query_Planner should return planned queries where every category is a member of the valid SearchCategory enum (fashion, finance, business, tech).
Validates: Requirements 1.2
Property 4: Specified categories are preserved in planned queries
For any set of specified Search_Categories, all planned queries produced by the Query_Planner should only reference categories from the specified set.
Validates: Requirements 1.3
Property 5: Query planner always produces sub-queries
For any valid Search_Query and Company_Context, the Query_Planner should return at least one PlannedQuery.
Validates: Requirements 2.1
Property 6: Every sub-query triggers a search call
For any list of PlannedQueries, the web search executor should invoke the search provider exactly once per sub-query (before retries).
Validates: Requirements 3.1
Property 7: Synthesizer output structure
For any set of at least 5 raw search results, the Content_Synthesizer should produce exactly 5 SearchResult objects, each containing a non-empty
sourceUrl, a non-emptysummary, and a non-emptydescription.Validates: Requirements 4.1, 4.2
Property 8: Source URL traceability
For any output from the Content_Synthesizer, every
sourceUrlin the results should be present in the set of URLs from the raw input results.Validates: Requirements 4.3
Property 9: Persistence round-trip
For any completed trend search, persisting the Result_Set and then retrieving it by job ID should return an equivalent Result_Set, including the original query, company context, categories, and a timestamp.
Validates: Requirements 5.1, 5.2, 5.3
Property 10: Company data isolation
For any two distinct company IDs, trend search results persisted under company A should never appear when querying results for company B.
Validates: Requirements 5.4
Property 11: Successful pipeline sets completed status
For any trend search pipeline that completes all steps without error, the job record status should be "completed" and the
completedAttimestamp should be non-null.Validates: Requirements 6.4
Property 12: Input serialization round-trip
For any valid TrendSearchInput, serializing it to the Inngest event JSON payload and then deserializing it back should produce an object equal to the original input.
Validates: Requirements 8.1, 8.2
Error Handling
Testing Strategy
Property-Based Testing
Use
fast-checkas the property-based testing library (already compatible with the Jest setup in this project).Each correctness property maps to a single property-based test with a minimum of 100 iterations. Tests should be tagged with the property reference.
Tag format:
Feature: ai-trend-search-engine, Property {N}: {title}Key property tests:
Unit Testing
Unit tests complement property tests for specific examples and edge cases:
Integration Testing