-
Notifications
You must be signed in to change notification settings - Fork 192
feat: Add outreach-personalizer kit #191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0c783f0
60cb10f
d915073
b9391b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Flow-level API Keys (Required in Lamatic Studio / deployments) | ||
| FIRECRAWL_API_KEY=your_firecrawl_api_key | ||
| GROQ_API_KEY=your_groq_api_key |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| .lamatic/ | ||
| node_modules/ | ||
| .next/ | ||
| .env | ||
| .env.local |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # Outreach Personalizer - Personalized Outreach Generator | ||
|
|
||
| Outreach Personalizer is a web application that wraps a deployed Lamatic.ai AI flow. It automatically crawls a prospect company's website and founder's public profile, extracts concrete technical signals/milestones, connects them to a candidate's custom background, and drafts a highly personalized, high-specificity cold outreach email. | ||
|
|
||
| ## Key Features | ||
| - **Crawls Website Content**: Leverages Firecrawl integration inside the Lamatic flow to scrape company data. | ||
| - **Specific Signal Hook Extraction**: Uses an LLM to identify specific achievements, posts, or bugs rather than generic praises. | ||
| - **Value-Added Asset Suggestion**: Suggests a concrete asset the pitch candidate could build in under 2 hours to showcase capabilities. | ||
| - **Tailored Outreach Generation**: Drafts a clean, direct outreach email (80-120 words) with a specific call to action. | ||
| - **Premium Dark UI**: Glassmorphic, modern responsive web dashboard with step-by-step agent feedback and copy support. | ||
| - **Safe Server-Side Calls**: Never exposes secret credentials to the client side. | ||
| - **Resilient Error Interception**: Catches non-JSON / HTML gateway errors gracefully. | ||
|
|
||
| --- | ||
|
|
||
| ## Setup Instructions | ||
|
|
||
| ### Prerequisites | ||
| - Node.js v18.0.0 or higher | ||
| - npm or yarn | ||
|
|
||
| ### 1. Configure Environment Variables | ||
| Create a file named `.env.local` in the `apps/web/` directory (or copy it from the workspace root) and define the following variables: | ||
|
|
||
| ```env | ||
| # The secret credential used to authenticate your client requests (do not share or commit) | ||
| LAMATIC_API_KEY=your_lamatic_api_key | ||
|
|
||
| # The project identifier in the Lamatic Cloud Studio | ||
| LAMATIC_PROJECT_ID=your_lamatic_project_id | ||
|
|
||
| # The base URL endpoint for your deployed project (e.g. https://<org>-<project>.lamatic.dev) | ||
| LAMATIC_API_URL=your_lamatic_api_url | ||
|
|
||
| # The UUID indicating which specific flow to trigger (e.g. Outreach Personalizer) | ||
| FLOW_ID=your_deployed_flow_id | ||
| ``` | ||
|
|
||
| > [!IMPORTANT] | ||
| > Never commit `.env.local` to source control. It is already added to `.gitignore` to prevent secret exposure. | ||
|
|
||
| ### 2. Install Dependencies | ||
| Change into the `apps/web` directory and install the required modules: | ||
| ```bash | ||
| cd apps/web | ||
| npm install | ||
| ``` | ||
|
|
||
| ### 3. Run Locally | ||
| Start the local development server: | ||
| ```bash | ||
| npm run dev | ||
| ``` | ||
| Open [http://localhost:3000](http://localhost:3000) in your browser to view the application. | ||
|
|
||
| --- | ||
|
|
||
| ## Architectural Details & Integration | ||
|
|
||
| The app initiates execution of the Lamatic flow using a Next.js Server Handler at `apps/web/app/api/generate/route.ts`. | ||
|
|
||
| ``` | ||
| ┌────────────────────────┐ | ||
| │ Next.js Client │ | ||
| └───────────┬────────────┘ | ||
| │ POST (JSON Inputs) | ||
| ▼ | ||
| ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ | ||
| │ Next.js Server App Router │ | ||
| │ │ | ||
| │ ┌────────────────────────┐ ┌──────────────────┐ │ | ||
| │ │ API Route Handler ├──────────────►│ Lamatic Client │ │ | ||
| │ │ (api/generate/route) │ Executes │ (lib/lamatic.ts) │ │ | ||
| │ └────────────────────────┘ Client └─────────┬────────┘ │ | ||
| └─────────────────────────────────────────────────────┼───────────────────────────────────────┘ | ||
| │ GraphQL POST Request | ||
| ▼ | ||
| ┌──────────────────────┐ | ||
| │ Lamatic.ai Gateway │ | ||
| └──────────────────────┘ | ||
| ``` | ||
|
|
||
| 1. **Client Trigger**: The client form sends `company_url`, `founder_linkedin_url`, and `candidate_context` to the local Next.js `/api/generate` handler. | ||
| 2. **GraphQL Wrapping**: The `LamaticClient` wrapper formats the GraphQL mutation request using `FLOW_ID` as the workflow identifier. | ||
| 3. **Execution**: The request is securely dispatched to the deployed endpoint (`LAMATIC_API_URL`) authorized by `LAMATIC_API_KEY`. | ||
| 4. **Resilient Parsing**: The response text is scanned for HTML content (e.g. Cloudflare error pages) to ensure that raw parse exceptions are intercepted and displayed to the user as a friendly warning. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Agent: Outreach Personalizer | ||
|
|
||
| ## Purpose & Overview | ||
| The **Outreach Personalizer** agent is designed to construct hyper-personalized cold outreach pitches for job candidates. Instead of writing generic flattery, it acts as an analytical, eager candidate who does deep research to find concrete, high-specificity hooks. It crawls the target's public content, extracts technical signals, devises a small asset (which can be completed in < 2 hours), and builds a confident, direct cold email pitch. | ||
|
|
||
| --- | ||
|
|
||
| ## Workflow Steps | ||
|
|
||
| The agent's logic is defined across a multi-step chain in `flows/average-teenager.ts`: | ||
|
|
||
| ```mermaid | ||
| graph TD | ||
| Start([API Trigger]) --> Scrape[Firecrawl Crawl] | ||
| Scrape --> SignalLLM[LLM Node: Extract Signals] | ||
| SignalLLM --> AssetLLM[LLM Node: Devise Asset] | ||
| AssetLLM --> PitchLLM[LLM Node: Draft Email] | ||
| PitchLLM --> Response([Response Output]) | ||
| ``` | ||
|
|
||
| 1. **Firecrawl Node (`firecrawlNode_692`)**: Scrapes both the company website (`company_url`) and the founder's LinkedIn profile (`founder_linkedin_url`) provided in the trigger inputs. | ||
| 2. **Signal Extraction (`LLMNode_276`)**: Parses the crawled markdown content to extract 3-5 concrete "noticed things" (e.g. a specific feature release, a bug, a technical post, or a design decision) and rates them with a specificity score from 1 to 5. | ||
| 3. **Asset Suggestion (`LLMNode_996`)**: Connects the candidate's background with one of the extracted signals and suggests one small, concrete value-add asset (e.g. a mini bug list, a UX walkthrough, a content sample, or a competitor teardown) that the candidate can build quickly. | ||
| 4. **Outreach Drafting (`LLMNode_696`)**: Selects the signal with the highest specificity score and drafts a direct, short cold outreach email around it. | ||
|
|
||
| --- | ||
|
|
||
| ## Agent Persona & Style Constraints | ||
|
|
||
| - **Tone**: Conversational, confident, direct, and non-corporate. Sounds like one builder writing to another. | ||
| - **Formality**: Low-to-moderate; avoids generic professional platitudes. | ||
| - **Length**: Strict constraint of 80 to 120 words. | ||
| - **Structure**: | ||
| 1. Hook based on the highest specificity signal (quotes/paraphrases the exact metric/name/result). | ||
| 2. Connection to the candidate's background. | ||
| 3. Description of the specific value-add asset. | ||
| 4. Ends with exactly: *"Would it make sense to speak for 10 minutes?"* | ||
| - **Banned Words & Phrases**: | ||
| - *"I'd love to"* | ||
| - *"could be really useful"* | ||
| - *"I hope this finds you well"* | ||
| - *"reach out"* | ||
| - *"circle back"* | ||
| - *"leverage"* | ||
| - Em-dashes (`—` or `--`) | ||
|
|
||
| --- | ||
|
|
||
| ## API Inputs & Outputs | ||
|
|
||
| ### Expected Inputs | ||
| The trigger accepts a JSON payload with the following fields: | ||
| - `company_url` (string): Target company website. | ||
| - `founder_linkedin_url` (string): Founder's LinkedIn profile. | ||
| - `candidate_context` (string): Context about the candidate's background, skills, and projects. | ||
|
|
||
| ### Expected Outputs | ||
| The flow execution response contains: | ||
| - `status` (string): Execution status (e.g. `success`). | ||
| - `result` (object/string): The plain text cold outreach email, along with outputs from preceding nodes (signals and asset ideas) if mapped. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| LAMATIC_API_KEY=lt-xxxxxx | ||
| LAMATIC_PROJECT_ID=xxxxxx | ||
| LAMATIC_API_URL=https://xxxxxx.lamatic.dev | ||
| FLOW_ID=xxxxxx |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
|
||
| # dependencies | ||
| /node_modules | ||
| /.pnp | ||
| .pnp.* | ||
| .yarn/* | ||
| !.yarn/patches | ||
| !.yarn/plugins | ||
| !.yarn/releases | ||
| !.yarn/versions | ||
|
|
||
| # testing | ||
| /coverage | ||
|
|
||
| # next.js | ||
| /.next/ | ||
| /out/ | ||
|
|
||
| # production | ||
| /build | ||
|
|
||
| # misc | ||
| .DS_Store | ||
| *.pem | ||
|
|
||
| # debug | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| .pnpm-debug.log* | ||
|
|
||
| # env files (can opt-in for committing if needed) | ||
| .env* | ||
| !.env.example | ||
|
|
||
| # vercel | ||
| .vercel | ||
|
|
||
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <!-- BEGIN:nextjs-agent-rules --> | ||
| # This is NOT the Next.js you know | ||
|
|
||
| This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. | ||
| <!-- END:nextjs-agent-rules --> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| @AGENTS.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # Outreach Personalizer - Next.js Reference App | ||
|
|
||
| This is the reference web application for the **Outreach Personalizer** kit. It provides a modern, responsive web dashboard where job candidates can generate hyper-personalized cold outreach emails based on a target company website URL, founder's LinkedIn URL, and the candidate's background. | ||
|
|
||
| The application triggers a deployed Lamatic.ai AI flow via Next.js Server Actions, keeping all API credentials secure on the server side. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Input Form**: Clean fields for the company website, founder LinkedIn profile, and candidate background context. | ||
| - **Real-Time Step Progression**: Dynamic feedback showing the current step of the underlying agent flow. | ||
| - **Copy-to-Clipboard**: Quick copy functionality for the generated cold outreach pitch. | ||
| - **Error Interception**: Graceful handling of network failures or flow timeout errors. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Node.js v18.0.0 or higher | ||
| - npm, yarn, or pnpm | ||
|
|
||
| ## Environment Setup | ||
|
|
||
| Create a `.env.local` file in this directory (`apps/`), or copy `.env.example` to `.env.local`: | ||
|
|
||
| ```bash | ||
| cp .env.example .env.local | ||
| ``` | ||
|
|
||
| Define the following environment variables inside `.env.local`: | ||
|
|
||
| ```env | ||
| # The secret credential used to authenticate your client requests (do not share or commit) | ||
| LAMATIC_API_KEY=your_lamatic_api_key | ||
|
|
||
| # The project identifier in the Lamatic Cloud Studio | ||
| LAMATIC_PROJECT_ID=your_lamatic_project_id | ||
|
|
||
| # The base URL endpoint for your deployed project (e.g. https://<org>-<project>.lamatic.dev) | ||
| LAMATIC_API_URL=your_lamatic_api_url | ||
|
|
||
| # The UUID indicating which specific flow to trigger (e.g. Outreach Personalizer) | ||
| FLOW_ID=your_deployed_flow_id | ||
| ``` | ||
|
|
||
| ## Running the Application | ||
|
|
||
| 1. **Install dependencies**: | ||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| 2. **Run the development server**: | ||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| 3. **Access the dashboard**: | ||
| Open [http://localhost:3000](http://localhost:3000) in your browser. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| "use server" | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| import { getLamaticClient } from "../lib/lamatic-client"; | ||
| import kitConfig from "../../lamatic.config"; | ||
|
|
||
| export interface PitchInput { | ||
| company_url: string; | ||
| founder_linkedin_url: string; | ||
| candidate_context: string; | ||
| } | ||
|
|
||
| const TIMEOUT_MS = 300000; // 5 minutes (Matching Vercel Hobby maxDuration) | ||
|
|
||
| /** | ||
| * Wraps a promise with a timeout. | ||
| */ | ||
| async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> { | ||
| let timer: NodeJS.Timeout | undefined; | ||
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| timer = setTimeout(() => reject(new Error(errorMessage)), timeoutMs); | ||
| // Ensure node can exit if the timer is the only thing keeping it alive | ||
| if (timer.unref) { | ||
| timer.unref(); | ||
| } | ||
| }); | ||
| try { | ||
| return await Promise.race([promise, timeoutPromise]); | ||
| } finally { | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export async function generatePersonalizedPitch(input: PitchInput): Promise<{ | ||
| success: boolean; | ||
| data?: any; | ||
| error?: string; | ||
| }> { | ||
| try { | ||
| console.log("[v0] Executing Average Teenager flow for company URL:", input.company_url); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value Debug tags survived the mission, but the sensitive payloads didn't.
Also applies to: 61-61 🤖 Prompt for AI Agents |
||
|
|
||
| const flowStep = kitConfig.steps.find((step) => step.id === "average-teenager"); | ||
| if (!flowStep) { | ||
| throw new Error("Step 'average-teenager' not defined in lamatic.config."); | ||
| } | ||
| const envKey = flowStep.envKey || "FLOW_ID"; | ||
| const flowId = process.env[envKey]; | ||
| if (!flowId) { | ||
| throw new Error(`${envKey} is not set in environment variables.`); | ||
| } | ||
|
|
||
| const trimmedInput = { | ||
| company_url: input.company_url.trim(), | ||
| founder_linkedin_url: input.founder_linkedin_url.trim(), | ||
| candidate_context: input.candidate_context.trim(), | ||
| }; | ||
|
|
||
| const resData: any = await withTimeout( | ||
| getLamaticClient().executeFlow(flowId, trimmedInput), | ||
| TIMEOUT_MS, | ||
| "The AI provider is taking longer than usual to respond. This usually means their service is overloaded. Please try again in a few minutes." | ||
| ); | ||
|
|
||
| if (!resData || (resData.status && resData.status === "error")) { | ||
| throw new Error(resData.message || "Flow execution failed or returned no result."); | ||
| } | ||
|
|
||
| return { | ||
| success: true, | ||
| data: resData.result, | ||
| }; | ||
| } catch (error: any) { | ||
| console.error("[v0] Generation error:", error); | ||
|
|
||
| let errorMessage = "An unexpected error occurred during execution."; | ||
| if (error instanceof Error) { | ||
| errorMessage = error.message; | ||
| if (error.message.includes("fetch failed")) { | ||
| errorMessage = "Network error: Unable to connect to the Lamatic service. Please check your internet connection."; | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| @import "tailwindcss"; | ||
|
|
||
| :root { | ||
| --background: #ffffff; | ||
| --foreground: #171717; | ||
| } | ||
|
|
||
| @theme inline { | ||
| --color-background: var(--background); | ||
| --color-foreground: var(--foreground); | ||
| --font-sans: var(--font-geist-sans); | ||
| --font-mono: var(--font-geist-mono); | ||
| } | ||
|
|
||
| @media (prefers-color-scheme: dark) { | ||
| :root { | ||
| --background: #0a0a0a; | ||
| --foreground: #ededed; | ||
| } | ||
| } | ||
|
|
||
| body { | ||
| background: var(--background); | ||
| color: var(--foreground); | ||
| font-family: Arial, Helvetica, sans-serif; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Setup guide describes an architecture that doesn't exist — self-destruct these instructions before someone follows them.
This document sends the reader to
apps/web/(lines 23, 45) and describes execution viaapps/web/app/api/generate/route.tscalling alib/lamatic.tswrapper (lines 60, 73-74, 84) using a GraphQL POST diagram. The actual kit implementation uses:apps/(notapps/web/)generatePersonalizedPitchinapps/actions/orchestrate.ts, not an API route handlerapps/lib/lamatic-client.ts, notlib/lamatic.tsFollowing these instructions verbatim (
cd apps/web) will fail immediately, and the architecture section misrepresents how the flow is actually invoked.📝 Proposed fix (representative excerpt)
Also applies to: 45-46, 60-86
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 22-22: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents