feat: Add outreach-personalizer kit#191
Conversation
Adds a new AgentKit kit that generates personalized cold outreach pitches from a company website URL, founder LinkedIn URL, and candidate background context. - Firecrawl-based company research node - LLM pitch generation with custom constitution (tone, identity, data-handling rules) - Next.js reference app (apps/) demonstrating end-to-end usage via the Lamatic SDK Verified via end-to-end test through the live reference app UI with real company/LinkedIn/candidate inputs; output confirmed clean with no console/terminal errors.
WalkthroughThis PR adds the outreach-personalizer kit: a Lamatic flow and prompt stack, a Next.js app that executes it through a server action, and the supporting app shell, configuration, and documentation. ChangesOutreach Personalizer kit
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
:robot_face: AgentKit Structural ValidationNew Contributions Detected
Check Results
🎉 All checks passed! This contribution follows the AgentKit structure. |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@kits/outreach-personalizer/agent.md`:
- Around line 21-24: The agent doc is overstating what the flow actually crawls:
`firecrawlNode_692` in the outreach-personalizer flow only receives
`company_url`, not a founder LinkedIn URL or other dynamically constructed URLs.
Update the description in the agent doc to match the real wiring, or extend the
flow so `firecrawlNode_692` also accepts and crawls the founder URL before
keeping the broader wording.
In `@kits/outreach-personalizer/apps/actions/orchestrate.ts`:
- Line 17: Remove the scaffold debug logging that leaks sensitive data from
orchestrate; the `console.log` in `orchestrate` should not print `input`, and
the raw SDK response dump should also be removed or replaced with a minimal,
non-PII status message. Keep only safe metadata in logs by updating the
`orchestrate` flow’s logging around the `[v0]` statements and any response
handling so user-submitted `candidate_context` and raw payload contents are
never written to server logs.
- Line 30: Wrap the lamaticClient.executeFlow call in orchestrate with a local
timeout so the server action cannot hang indefinitely if the flow stalls. Use a
timeout helper around executeFlow in orchestrate.ts, and on timeout return or
throw a clear error that the flow timed out; keep the existing trimmedInput and
flowId usage unchanged.
In `@kits/outreach-personalizer/apps/app/page.tsx`:
- Around line 178-238: The form fields in this section are missing programmatic
label associations because the labels are separate from the inputs in the page
component. Update the Company Website URL, Founder LinkedIn URL, and Candidate
Background & Context fields in app/page.tsx so each <label> uses a matching
htmlFor and each input/textarea has the corresponding id, or wrap the control
inside the label. Use the existing field blocks and related state handlers
(companyUrl, founderLinkedinUrl, candidateContext) to keep the structure aligned
while making the labels accessible.
In `@kits/outreach-personalizer/apps/lib/lamatic-client.ts`:
- Around line 4-8: The FLOW_ID validation in lamatic-client.ts is in the wrong
module and duplicates the real guard already performed in orchestrate.ts. Remove
the process.env.FLOW_ID check from the client initialization path and keep
lamatic-client focused on client setup only; leave FLOW_ID validation in
orchestrate.ts where the flow invocation actually happens. Use the lamatic
client setup code and the orchestrate.ts flow entrypoint as the symbols to
locate the affected logic.
- Around line 10-20: The module-scope environment guard in lamaticClient is
throwing during import, which can break page build/static generation before
runtime. Move the validation out of the top-level of lamatic-client.ts and into
a lazy initializer or the first place Lamatic is actually used (for example
inside orchestrate or a createLamaticClient helper), so page.tsx can import the
module without immediately requiring LAMATIC_API_URL, LAMATIC_PROJECT_ID, and
LAMATIC_API_KEY. Keep the existing Lamatic constructor setup, but only run the
env check when the client is actually needed.
In `@kits/outreach-personalizer/apps/README.md`:
- Around line 1-36: The README is still the default create-next-app template and
does not document the outreach-personalizer kit workflow. Replace the generic
content with kit-specific setup instructions that explain how to configure the
Lamatic environment variables, run the reference app end to end, and any
required prerequisites. Update the README content in place so it reflects the
shipped kit rather than the starter Next.js app, and make sure the guidance is
tailored to this app’s setup and usage.
In `@kits/outreach-personalizer/constitutions/default.md`:
- Around line 3-31: The Markdown in the default constitution is missing blank
lines around the `##` headings, causing MD022 failures. Update the `##
Identity`, `## Safety`, `## Data Handling`, `## Tone & Style Guidelines`, `##
Banned Words & Symbols`, and `## Structural Rules` sections so each heading has
a blank line before and after it, while keeping the existing content and
structure intact.
In `@kits/outreach-personalizer/flows/average-teenager.ts`:
- Around line 20-31: The Firecrawl input mapping is dropping
founder_linkedin_url, so the founder-provided URL never reaches downstream
research or prompt steps. Update firecrawlNode_692 in average-teenager.ts to
pass through founder_linkedin_url alongside company_url, and make sure the input
schema/field wiring matches the downstream contract used by orchestrate so both
sources are preserved end-to-end.
In `@kits/outreach-personalizer/prompts/average-teenager_llmnode-276_system_0.md`:
- Around line 1-5: The extraction prompt is vulnerable to prompt injection
because it treats scraped page text as instruction-bearing content. Update the
prompt around the main extraction flow to explicitly tell the model to ignore
any commands, directives, or role instructions found in the crawl output and to
treat the text only as untrusted data. Keep the task focused on extracting 3-5
specific noticed things with source and specificity_score, and reinforce this
guardrail in the prompt text used by the LLM node.
In `@kits/outreach-personalizer/README.md`:
- Around line 22-23: The README setup and architecture instructions reference
the wrong app structure and execution flow, so update them to match the actual
kit implementation. Replace mentions of apps/web with the real apps directory,
and describe the invocation path through the generatePersonalizedPitch Server
Action in apps/actions/orchestrate.ts using the lamaticClient wrapper in
apps/lib/lamatic-client.ts instead of an API route or lib/lamatic.ts. Keep the
environment/setup steps aligned with the real directory layout so readers are
not sent to nonexistent paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: d963c140-3478-42ec-8f6b-6c37abf396c4
⛔ Files ignored due to path filters (7)
kits/outreach-personalizer/apps/app/favicon.icois excluded by!**/*.icokits/outreach-personalizer/apps/package-lock.jsonis excluded by!**/package-lock.jsonkits/outreach-personalizer/apps/public/file.svgis excluded by!**/*.svgkits/outreach-personalizer/apps/public/globe.svgis excluded by!**/*.svgkits/outreach-personalizer/apps/public/next.svgis excluded by!**/*.svgkits/outreach-personalizer/apps/public/vercel.svgis excluded by!**/*.svgkits/outreach-personalizer/apps/public/window.svgis excluded by!**/*.svg
📒 Files selected for processing (30)
kits/outreach-personalizer/.gitignorekits/outreach-personalizer/README.mdkits/outreach-personalizer/agent.mdkits/outreach-personalizer/apps/.env.examplekits/outreach-personalizer/apps/.gitignorekits/outreach-personalizer/apps/AGENTS.mdkits/outreach-personalizer/apps/CLAUDE.mdkits/outreach-personalizer/apps/README.mdkits/outreach-personalizer/apps/actions/orchestrate.tskits/outreach-personalizer/apps/app/globals.csskits/outreach-personalizer/apps/app/layout.tsxkits/outreach-personalizer/apps/app/page.tsxkits/outreach-personalizer/apps/eslint.config.mjskits/outreach-personalizer/apps/lib/lamatic-client.tskits/outreach-personalizer/apps/next.config.tskits/outreach-personalizer/apps/package.jsonkits/outreach-personalizer/apps/postcss.config.mjskits/outreach-personalizer/apps/tsconfig.jsonkits/outreach-personalizer/constitutions/default.mdkits/outreach-personalizer/flows/average-teenager.tskits/outreach-personalizer/lamatic.config.tskits/outreach-personalizer/model-configs/average-teenager_llmnode-276_generative-model-name.tskits/outreach-personalizer/model-configs/average-teenager_llmnode-696_generative-model-name.tskits/outreach-personalizer/model-configs/average-teenager_llmnode-996_generative-model-name.tskits/outreach-personalizer/prompts/average-teenager_llmnode-276_system_0.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-276_user_1.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-696_system_0.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-696_user_1.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-996_system_0.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-996_user_1.md
| ### 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: |
There was a problem hiding this comment.
📐 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 via apps/web/app/api/generate/route.ts calling a lib/lamatic.ts wrapper (lines 60, 73-74, 84) using a GraphQL POST diagram. The actual kit implementation uses:
- Directory
apps/(notapps/web/) - A Server Action
generatePersonalizedPitchinapps/actions/orchestrate.ts, not an API route handler - The client wrapper at
apps/lib/lamatic-client.ts, notlib/lamatic.ts
Following these instructions verbatim (cd apps/web) will fail immediately, and the architecture section misrepresents how the flow is actually invoked.
📝 Proposed fix (representative excerpt)
-Create a file named `.env.local` in the `apps/web/` directory (or copy it from the workspace root) and define the following variables:
+Create a file named `.env.local` in the `apps/` directory (or copy it from the workspace root) and define the following variables:-Change into the `apps/web` directory and install the required modules:
+Change into the `apps/` directory and install the required modules:
```diff
-The app initiates execution of the Lamatic flow using a Next.js Server Handler at `apps/web/app/api/generate/route.ts`.
+The app initiates execution of the Lamatic flow using a Next.js Server Action (`generatePersonalizedPitch`) defined in `apps/actions/orchestrate.ts`, which calls the `lamaticClient` in `apps/lib/lamatic-client.ts`.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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@kits/outreach-personalizer/README.md` around lines 22 - 23, The README setup
and architecture instructions reference the wrong app structure and execution
flow, so update them to match the actual kit implementation. Replace mentions of
apps/web with the real apps directory, and describe the invocation path through
the generatePersonalizedPitch Server Action in apps/actions/orchestrate.ts using
the lamaticClient wrapper in apps/lib/lamatic-client.ts instead of an API route
or lib/lamatic.ts. Keep the environment/setup steps aligned with the real
directory layout so readers are not sent to nonexistent paths.
…ning - Wire founder_linkedin_url into Firecrawl node - Remove debug logs leaking candidate_context and raw SDK responses - Add timeout wrapper around executeFlow - Harden signal-extraction prompt against prompt injection - Fix agent.md to match actual flow behavior - Add htmlFor/id pairs to form labels for accessibility - Add blank lines around headings in constitution (MD022) - Remove redundant FLOW_ID check in lamatic-client.ts - Rewrite apps/README.md with kit-specific setup docs - Add kit-level .env.example for flow-level API keys
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@kits/outreach-personalizer/apps/actions/orchestrate.ts`:
- Line 33: The remaining debug-style logging in orchestrate() still leaks
implementation noise, so clean up the console.log calls tied to the flow
execution and error handling by removing the “[v0]” prefix and avoiding logging
sensitive inputs like company_url or raw error objects. Update the affected log
statements in orchestrate() to use neutral, non-debug messages and keep only the
minimal context needed for normal operation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0d813d41-4a46-4aa9-947f-41179902708d
📒 Files selected for processing (10)
kits/outreach-personalizer/.env.examplekits/outreach-personalizer/agent.mdkits/outreach-personalizer/apps/README.mdkits/outreach-personalizer/apps/actions/orchestrate.tskits/outreach-personalizer/apps/app/page.tsxkits/outreach-personalizer/apps/lib/lamatic-client.tskits/outreach-personalizer/constitutions/default.mdkits/outreach-personalizer/flows/average-teenager.tskits/outreach-personalizer/prompts/average-teenager_llmnode-276_system_0.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-276_user_1.md
💤 Files with no reviewable changes (1)
- kits/outreach-personalizer/apps/lib/lamatic-client.ts
| error?: string; | ||
| }> { | ||
| try { | ||
| console.log("[v0] Executing Average Teenager flow for company URL:", input.company_url); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Debug tags survived the mission, but the sensitive payloads didn't.
candidate_context and the raw SDK dump are gone — good. The [v0] log prefixes and the company_url/error-object logging are cosmetic remnants; not a blocker, low priority.
Also applies to: 61-61
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@kits/outreach-personalizer/apps/actions/orchestrate.ts` at line 33, The
remaining debug-style logging in orchestrate() still leaks implementation noise,
so clean up the console.log calls tied to the flow execution and error handling
by removing the “[v0]” prefix and avoiding logging sensitive inputs like
company_url or raw error objects. Update the affected log statements in
orchestrate() to use neutral, non-debug messages and keep only the minimal
context needed for normal operation.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
kits/outreach-personalizer/apps/actions/orchestrate.ts (1)
16-25: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
withTimeoutleaks an unhandled rejection when the main promise resolves first.When
executeFlowresolves before the 5-minute timeout,Promise.racesettles but thesetTimeouttimer is still pending. When it eventually fires, it rejects thetimeoutPromisewith no consumer — an unhandled promise rejection. On Node.js 15+ (where--unhandled-rejections=throwis the default), this can crash the server process.Clear the timer once the race settles.
🔒 Proposed fix
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> { + let timer: ReturnType<typeof setTimeout>; const timeoutPromise = new Promise<never>((_, reject) => { - const timer = setTimeout(() => reject(new Error(errorMessage)), timeoutMs); + 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(); } }); - return Promise.race([promise, timeoutPromise]); + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + clearTimeout(timer); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@kits/outreach-personalizer/apps/actions/orchestrate.ts` around lines 16 - 25, The withTimeout helper in orchestrate.ts leaves the timeout pending after Promise.race settles, so a later timeout can reject without a consumer and trigger an unhandled rejection. Update withTimeout to keep the timeout handle, clear it as soon as the wrapped promise resolves or rejects, and still preserve the timeout error path; use the withTimeout function and its internal setTimeout/Promise.race logic as the place to fix this.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@kits/outreach-personalizer/apps/actions/orchestrate.ts`:
- Around line 1-2: The orchestrate.ts action is missing the parent kit
configuration import, so the step-definition source is not being registered.
Update the module that contains getLamaticClient() and FLOW_ID usage to also
import ../../lamatic.config, ensuring the orchestration flow loads the required
kit config before execution. Keep the fix localized to orchestrate.ts and
preserve the existing server action setup.
---
Outside diff comments:
In `@kits/outreach-personalizer/apps/actions/orchestrate.ts`:
- Around line 16-25: The withTimeout helper in orchestrate.ts leaves the timeout
pending after Promise.race settles, so a later timeout can reject without a
consumer and trigger an unhandled rejection. Update withTimeout to keep the
timeout handle, clear it as soon as the wrapped promise resolves or rejects, and
still preserve the timeout error path; use the withTimeout function and its
internal setTimeout/Promise.race logic as the place to fix this.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: a4fe28f4-5463-47f0-83f6-edb8312a238c
📒 Files selected for processing (2)
kits/outreach-personalizer/apps/actions/orchestrate.tskits/outreach-personalizer/apps/lib/lamatic-client.ts
…en turbopack root
|
@coderabbitai review |
✅ Action performedReview finished.
|
Adds a new AgentKit kit that generates personalized cold outreach pitches from a company website URL, founder LinkedIn URL, and candidate background context.
Verified via end-to-end test through the live reference app UI with real company/LinkedIn/candidate inputs; output confirmed clean with no console/terminal errors.
PR Checklist
1. Select Contribution Type
kits/<category>/<kit-name>/)bundles/<bundle-name>/)templates/<template-name>/)2. General Requirements
kebab-caseand matches the flow IDREADME.md(purpose, setup, usage)3. File Structure (Check what applies)
config.jsonpresent with valid metadata (name, description, tags, steps, author, env keys)flows/<flow-name>/(where applicable) include:config.json(Lamatic flow export)inputs.jsonmeta.jsonREADME.md.env.examplewith placeholder values only (kits only)config.jsonnode graphs (changes via Lamatic Studio export)4. Validation
npm install && npm run devworks locally (kits: UI runs; bundles/templates: flows are valid)[kit] Add <name> for <use case>)Added
kits/outreach-personalizer/.gitignore(ignores.lamatic/,node_modules/,.next/, and local env files).Added
kits/outreach-personalizer/README.md(what the kit/app does, required env vars, and architecture: Next.js route → Lamatic flow viaLamaticClient, with friendly handling for non-JSON/HTML gateway errors).Added
kits/outreach-personalizer/agent.md(agent objective and workflow: Firecrawl company/founder crawl → extract 3–5 “noticed things” → propose a buildable value-add asset → draft final personalized cold email; includes tone/format rules, banned phrases, and no em-dashes).Added
kits/outreach-personalizer/.env.example(exampleFIRECRAWL_API_KEYandGROQ_API_KEY).Added
kits/outreach-personalizer/constitutions/default.md(Default Constitution: identity/tone rules, jailbreak/prompt-injection refusal, and strict PII/data-handling + exact 80–120 word plain-text email + exact closing sentence).Added
kits/outreach-personalizer/lamatic.config.ts(kit metadata; single stepaverage-teenagerwithenvKey: "FLOW_ID").Added
kits/outreach-personalizer/flows/average-teenager.ts(full flow graph + references to prompts/constitutions/model configs).Added model-config files:
kits/outreach-personalizer/model-configs/average-teenager_llmnode-276_generative-model-name.tskits/outreach-personalizer/model-configs/average-teenager_llmnode-696_generative-model-name.tskits/outreach-personalizer/model-configs/average-teenager_llmnode-996_generative-model-name.tsAdded prompt files:
kits/outreach-personalizer/prompts/average-teenager_llmnode-276_system_0.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-276_user_1.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-996_system_0.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-996_user_1.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-696_system_0.mdkits/outreach-personalizer/prompts/average-teenager_llmnode-696_user_1.mdAdded Next.js reference app under
kits/outreach-personalizer/apps/:kits/outreach-personalizer/apps/.env.examplekits/outreach-personalizer/apps/.gitignorekits/outreach-personalizer/apps/AGENTS.mdkits/outreach-personalizer/apps/CLAUDE.mdkits/outreach-personalizer/apps/README.mdkits/outreach-personalizer/apps/actions/orchestrate.ts(server action:PitchInput+generatePersonalizedPitch; trims inputs, derivesFLOW_IDfromlamatic.config, callsgetLamaticClient().executeFlow(...)with a 5-minute timeout, returns{success, data|error}).kits/outreach-personalizer/apps/lib/lamatic-client.ts(lazy singletonLamaticclient with runtime env validation).kits/outreach-personalizer/apps/app/globals.csskits/outreach-personalizer/apps/app/layout.tsxkits/outreach-personalizer/apps/app/page.tsx(UI form + step/progress state; renders generated pitch and optional intermediate node output; copy-to-clipboard).kits/outreach-personalizer/apps/app/favicon.icokits/outreach-personalizer/apps/app/eslint.config.mjskits/outreach-personalizer/apps/next.config.ts(setsturbopack.rootto parent directory).kits/outreach-personalizer/apps/package.jsonkits/outreach-personalizer/apps/package-lock.jsonkits/outreach-personalizer/apps/postcss.config.mjskits/outreach-personalizer/apps/tsconfig.jsonkits/outreach-personalizer/apps/public/file.svg,globe.svg,next.svg,vercel.svg,window.svgFlow behavior (from
kits/outreach-personalizer/flows/average-teenager.ts)triggerNode(triggerNode_1/graphqlNode): realtime API trigger that acceptscompany_url,founder_linkedin_url,candidate_contextand advances viato-response.dynamicNode:firecrawlNode_692:syncBatchScrapeovercompany_url+founder_linkedin_urlusing provided Firecrawl credentials.LLMNode_276: extracts 3–5 “noticed things” withspecificity_score(promptaverage-teenager_llmnode-276_*).LLMNode_996: proposes one buildable value-add asset under 2 hours (promptaverage-teenager_llmnode-996_*).LLMNode_696: drafts the final cold email/pitch in 80–120 words with the required structure/constraints (promptaverage-teenager_llmnode-696_*).responseNode(responseNode_triggerNode_1): returns the final flow output as JSON to the caller.triggerNode_1 → firecrawlNode_692(default edge)firecrawlNode_692 → LLMNode_276 → LLMNode_996 → LLMNode_696(default edges in sequence)LLMNode_696 → responseNode_triggerNode_1(default edge)triggerNode_1 → responseNode_triggerNode_1(aresponseEdgeusingsourceHandle: "to-response"/targetHandle: "from-trigger"), ensuring the API trigger’s response is populated from the flow result.