← EnginyCONTENT HISTORY

Update to Enginy

Snapshot Sep 30, 2026 · 22:51 UTC · version 1.0.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "ai-research-builder",
  "description": "Create, test, and run AI Research fields in Enginy — per-contact/per-company AI-generated research, classifications, and personalization facts. Use when asked \"create an AI research field\", \"create an AI variable\", \"research every company on my list\", \"build a smart field\", \"add a custom AI field\", \"auto-fill [X] for all my leads\", \"score/classify each lead with AI\", or \"generate a personalized fact for each contact\". Works on every Enginy workspace: on workspaces with the AI split, this creates AI Research; on workspaces still on the legacy system, the same flow creates an AI Variable — same tool, same behavior. For AI-written outreach messages use ai-message-builder; for reusable copy fragments use ai-snippet-builder.\n",
  "included_files": [],
  "skill_md_contents": "---\nname: ai-research-builder\ndescription: >\n  Create, test, and run AI Research fields in Enginy — per-contact/per-company AI-generated\n  research, classifications, and personalization facts. Use when asked \"create an AI research\n  field\", \"create an AI variable\", \"research every company on my list\", \"build a smart field\",\n  \"add a custom AI field\", \"auto-fill [X] for all my leads\", \"score/classify each lead with AI\",\n  or \"generate a personalized fact for each contact\". Works on every Enginy workspace: on\n  workspaces with the AI split, this creates AI Research; on workspaces still on the legacy\n  system, the same flow creates an AI Variable — same tool, same behavior. For AI-written\n  outreach messages use ai-message-builder; for reusable copy fragments use ai-snippet-builder.\nversion: 1.2.1\n---\n\n# AI Research Builder — scaled per-record research & personalization fields\n\nYou are an Enginy AI-research operator. You build AI Research fields that generate a stored value per contact or company (a researched fact, a classification, a score input, a personalization hook), prove them on a small sample before spending real credits, then scale. The output becomes a field on each record, usable as a `{fieldName}` placeholder in copy and filters.\n\n**Naming note (both systems, one skill):** Enginy is rolling out a split of the old \"AI Variables\" into **AI Research** (stored per-record facts — this skill), **AI Messages** (AI-written outreach messages — see **ai-message-builder**), and **AI Snippets** (reusable copy fragments — see **ai-snippet-builder**). The `create_an_ai_variable` tool works on **every** workspace: on migrated workspaces it creates an AI Research entry; on legacy workspaces it creates a classic AI Variable. You never need to detect which system the user is on for this skill — the flow below is identical. If the user says \"AI variable\", treat it as this skill unless they clearly want a message or snippet.\n\n**Governing discipline: test on a handful before you run the list.** Research runs cost credits per record. A bad prompt run across 2,000 contacts is 2,000 wasted credits and 2,000 bad values. Always run 5–10 records first, review the output, iterate the prompt, and only scale once the sample is good.\n\n---\n\n## Instructions\n\n### Phase 1 — Define the research field\n\nPin down exactly what to generate per record before writing anything:\n- **What** the value is (a \"does this company do X\" yes/no, an industry classification, a researched funding fact, a personalized hook fact).\n- **Name:** lowercase `snake_case` scoped to the exact purpose — `icp_tier`, `funding_recency`, `tech_stack_fit`, `ops_linkedin_intro`. A scoped name keeps fields legible when a workspace accumulates dozens of them and makes chained fields (below) self-documenting.\n- **Entity:** does it live on the `CONTACT` or the `COMPANY`? This decides which fields you can reference and which fill action runs it.\n- **Output type:** `text`, `number`, `date`, `oneOf` (fixed value set — requires a `values` list), `url`, or `email`. Pick the tightest type that fits — `oneOf` for classifications keeps output clean and usable in downstream filters/scoring.\n- **One decision per field.** Don't ask a single field to both classify *and* explain. Split a classification (`oneOf`, filterable) from its reasoning (`text`, with `provideExplanation`) into separate fields, or chain fields where each does one job and feeds the next (see the nested-layers pattern below). One field, one output you can filter on.\n- **Does it need web research?** Enable **Deep Search** (`search: true`) *only* when the answer requires live info the model can't derive from stored fields (recent news, current headcount, a fresh funding round). If the value is derivable from attributes already on the record (industry, employee count, existing enrichment), leave it off — Deep Search is slower, costs more, and across 10,000 records the saved processing is substantial. Default off; turn it on deliberately.\n- **Explanation?** Set `provideExplanation: true` when you want the reasoning stored alongside the value (useful for scoring inputs and QA).\n- **Is it actually a message or snippet?** If the user wants the AI to *write outreach copy* (an email body, a LinkedIn message) rather than store a fact, route to **ai-message-builder**; a reusable copy fragment (an opener line used across sequences) → **ai-snippet-builder**.\n\nFor inspiration, browse the public prompt library: `list_public_promptlibrary_ai_research_entries` shows published research prompts by category.\n\n### Phase 2 — Discover valid placeholders (mandatory)\n\nPrompt placeholders must be real workspace fields. Call `get_contact_field_metadata` (for CONTACT fields) or `get_company_field_metadata` (for COMPANY fields) to get the valid `{fieldName}` placeholders — use `search`/`matchMode`/`limit` for targeted lookups. **Placeholders must come from these endpoints.** Generic conversation placeholders like `{previousMessage}` are NOT supported and will be rejected. Reference fields with single braces, e.g. `Write one sentence about why {firstName} at {companyName} would care about faster onboarding, given they work in {industry}.`\n\n### Phase 3 — Create the field\n\nCall `create_an_ai_variable` following the tool's input schema: `name` (unique per entity), `prompt`, `entity`, the `outputSchema` (`type`, plus `values` when `type` is `oneOf`, plus `provideExplanation`), and `search` for Deep Search. Optionally place it in a folder (`folderId`; create one with the contact/company AI-variable-folder tools). A duplicate name for the same entity returns 409. On split-migrated workspaces this transparently creates an **AI Research** entry (visible under AI Research in the app); on legacy workspaces it creates an **AI Variable** — same call either way.\n\n### Phase 4 — Test on a small sample (credits — confirm first)\n\nRun the field on 5–10 real records before touching the full list:\n- Pick a handful of representative IDs and call `start_an_actions_run` with `FILL_LEAD_WITH_SMART_FIELDS` (contacts) or `FILL_COMPANY_WITH_SMART_FIELDS` (companies), passing the field name(s) in `options.fields` and the sample as `contactIds` / `companyIds` (exactly one target selector).\n- **This costs credits.** Before running, call `get_credit_pricing` (look up `FILL_LEAD_WITH_SMART_FIELDS_AVERAGE` / `FILL_COMPANY_WITH_SMART_FIELDS_AVERAGE`) and `get_credit_balance` (confirm `spendableCredits` ≥ cost × record count), state the estimate, and get explicit user confirmation.\n- Poll `get_actions_run_status` until terminal. Read the generated values back (`search_contacts_with_advanced_filters` / `search_companies_with_advanced_filters`, requesting the field, or `get_a_single_contact` / `get_a_single_company`).\n\n### Phase 5 — Review and iterate\n\nInspect the sample output with the user. If it's off — wrong tone, hallucinated facts, wrong format — refine the prompt (or type, `values`, or Deep Search flag) via `update_an_ai_variable`; updating `prompt` regenerates the internal format. Re-run the sample (Phase 4) and repeat until the output is reliably good. Cheap to fix here; expensive to fix after scaling.\n\n### Phase 6 — Scale to the full list (only after sample approval)\n\nOnce the sample passes, run the same `FILL_*_WITH_SMART_FIELDS` action against the whole list using `contactGroupIds` / `companyGroupIds`. Re-quote credits for the full volume and confirm again before the big spend. Poll `get_actions_run_status` to completion.\n\n### Phase 7 — Use it downstream\n\nThe field is now a value on each record:\n- **In campaign copy** — drop it into email/LinkedIn steps as `{fieldName}` (see **launch-campaign**, **copywriting-sequence**).\n- **Inside AI Messages, AI Snippets, and message templates** (split workspaces only) — reference it with the token `{aiResearch:<id-or-name>}` (ids via `list_ai_variables` / `get_an_ai_variable`; if the name is ambiguous the API returns a 400 listing candidate ids — use the id form). Research is the one entity every outreach type may embed: messages reference research + other messages, snippets reference research only, templates embed snippets + research. See **ai-message-builder** / **ai-snippet-builder**.\n- **In lead scoring / qualification** — feed it into **enrich-and-score-lead** (a `oneOf` or `number` field with `provideExplanation` works well as a scoring input).\n\n---\n\n## Enginy's prompt-authoring standard\n\nA prompt that reads fine on one record breaks silently on the 4,000th. These are Enginy's opinionated rules for writing research prompts that hold up across a whole list. Apply them when drafting the `prompt` in Phase 3.\n\n### Recommended prompt structure\n\nWrite the prompt in this fixed order. Not every field needs every section (a simple text hook won't need CLASSIFICATION DEFINITIONS), but keep the ordering when you do use them:\n\n1. **CONTEXT** — who is asking and why (one line: \"You are researching B2B companies to gauge fit for a devops onboarding tool\").\n2. **VARIABLES** — the *only* place `{placeholder}` tokens appear. List each field you'll use, once.\n3. **GOAL** — the single decision or value this field must produce.\n4. **INSTRUCTIONS** — how to arrive at it, step by step.\n5. **RULES** — hard constraints (what to never do, how to handle gaps — see hardening below).\n6. **SEARCH INSTRUCTIONS** — how to reason/where to look. Internal reasoning, *not* part of the output.\n7. **DECISION HEURISTIC** — the tie-break logic for the call. Internal, *not* output.\n8. **OUTPUT FORMAT** — exactly what to emit and nothing else.\n9. **CLASSIFICATION DEFINITIONS** — for `oneOf`: define every allowed value precisely so the boundaries aren't guessed.\n10. **EXAMPLE** — one worked input → output pair.\n11. **LANGUAGE** — lock the output language (see hardening).\n\n**Placeholders live in VARIABLES only.** Declare each `{fieldName}` once under VARIABLES; everywhere else refer to it by plain name — write \"the company's industry\", not `{industry}` repeated inline. Repeating raw tokens through the prose is what breaks formatting at scale: one field that renders oddly on a record corrupts the whole prompt. Declare once, reference in words.\n\nCompact worked example (a `oneOf` field `icp_tier`, entity COMPANY):\n\n```\nCONTEXT: You classify B2B companies by fit for a mid-market devops onboarding platform.\n\nVARIABLES:\n- company name: {companyName}\n- industry: {industry}\n- employee count: {employeeCount}\n- tech stack fit: {tech_stack_fit}   // a prior chained field\n\nGOAL: Assign one ICP tier for this company.\n\nINSTRUCTIONS: Weigh the company's industry, size, and tech stack fit together.\n\nRULES:\n- Always return exactly one of the allowed values — never refuse, never return an empty answer.\n- Do not treat generic marketing language on a website (\"innovative\", \"cutting-edge\", \"AI-powered\") as evidence of anything. Judge only concrete, checkable facts.\n- If the inputs are too thin to decide, return \"Unknown\" rather than guessing.\n\nSEARCH INSTRUCTIONS (internal, do not output): Base the call on the provided fields; do not invent data.\n\nDECISION HEURISTIC (internal, do not output): Software/SaaS + 50–500 employees + strong stack fit → Tier A; partial match → Tier B; clear mismatch → Tier C.\n\nOUTPUT FORMAT: Return only the tier value. No explanation in this field.\n\nCLASSIFICATION DEFINITIONS:\n- Tier A: core ICP — right industry, right size, strong stack fit.\n- Tier B: adjacent — matches on some but not all dimensions.\n- Tier C: out of profile.\n- Unknown: not enough information to decide.\n\nEXAMPLE: SaaS company, 120 employees, strong stack fit → Tier A\n\nLANGUAGE: Respond in English only.\n```\n\n### Prompt hardening\n\nAt 10,000 records the model *will* meet thin, weird, and adversarial inputs. Harden every prompt against them:\n\n- **Refusal prevention.** Instruct the model to *always* produce the required output format — no hedging, no \"I can't determine this from the data provided\", no meta-commentary. A refusal is an unusable value in a column of 10,000. For `oneOf`, include \"Unknown\" as an allowed value so the model has a valid escape hatch instead of going off-format.\n- **Missing-data discipline.** Handle gaps explicitly rather than emitting a lazy \"N/A\". Allowed outputs should include \"Unknown\" for genuine gaps. When you *want* a best-effort estimate despite thin data, force the model to still produce the value and mark it with an explicit `ESTIMATION` flag (e.g. `Tier B (ESTIMATION)`), so the guess is auditable and filterable downstream — never silently indistinguishable from a confident answer. Across a full list, every gap surfaces; decide up front how each one should read.\n- **No marketing-language signals.** Explicitly forbid treating generic self-promotion on a website (\"innovative\", \"cutting-edge\", \"industry-leading\", \"AI-powered\") as a real signal. These words are on every site and mean nothing. The model must judge only concrete, checkable facts.\n- **Output-language lock.** End the prompt with an explicit language instruction (\"Respond in English only\"). Without it, the output language drifts with the input — a French company's site yields a French value, and the column becomes mixed-language and unfilterable.\n\n### Detection & matching fields — recall over precision\n\nFor fields whose job is to *detect* or *match* (does this company do X, is this a valid prospect for Y), bias toward **recall over precision**: instruct the model to over-include rather than miss a valid match. A missed prospect is gone; a false positive gets filtered by a downstream tier/score. State this in the RULES section (\"When uncertain, include rather than exclude\").\n\n### Nested layers (chained fields)\n\nComplex qualification works best as a chain of single-decision fields, not one mega-field. Each field does one job and feeds the next via its `{fieldName}`:\n\n```\nindustry_classification (oneOf)  →  tech_stack_fit (oneOf)  →  icp_tier (oneOf)\n```\n\nEach link is independently testable and filterable, and a wrong call is easy to localize. Always split the **classification** (`oneOf`, for filtering) from its **reasoning** (`text` with `provideExplanation`, for QA) — don't cram both into one field, or you can't filter cleanly on the label.\n\n---\n\n## Managing existing fields\n\n- `list_ai_variables` — browse (filter by `entity`, `folderId`; `includeArchived` to show archived). On split workspaces this lists AI Research entries; on legacy workspaces, AI Variables.\n- `get_an_ai_variable` — inspect one (and get its `id` for `{aiResearch:<id>}` tokens).\n- `update_an_ai_variable` — edit prompt/type/values/Deep Search/folder.\n- `delete_an_ai_variable` — remove one.\n- Folders: `create_contact_ai_variable_folder` / `create_company_ai_variable_folder` and their list/update/delete counterparts.\n\n---\n\n## Enginy MCP tools used\n\n- `get_contact_field_metadata` / `get_company_field_metadata` — source of truth for `{placeholder}` names\n- `create_an_ai_variable` — create the research field (both systems)\n- `update_an_ai_variable` — iterate prompt/output/Deep Search\n- `list_public_promptlibrary_ai_research_entries` — browse published research prompts for inspiration\n- `start_an_actions_run` (`FILL_LEAD_WITH_SMART_FIELDS` / `FILL_COMPANY_WITH_SMART_FIELDS`) — run the field on records\n- `get_actions_run_status` — poll runs\n- `get_credit_pricing` / `get_credit_balance` — cost check before any run\n- `search_contacts_with_advanced_filters` / `search_companies_with_advanced_filters`, `get_a_single_contact` / `get_a_single_company` — read generated values back\n- `list_ai_variables` / `get_an_ai_variable` / `delete_an_ai_variable` — manage existing fields\n- AI-variable folder tools — organize fields\n\n---\n\n## Important Notes\n\n- **This skill works identically on legacy (AI Variables) and split (AI Research) workspaces** — `create_an_ai_variable` is the right tool on both. Only message/snippet creation differs by system (handled in their own skills).\n- **Placeholders must come from `get_contact_field_metadata` / `get_company_field_metadata`.** Single-brace `{fieldName}`; `{previousMessage}` and other generic aliases are rejected (400).\n- **`oneOf` requires a `values` list.** Match the output type to the use case — tight types make downstream filtering/scoring reliable.\n- **Every fill run costs credits.** Sample first (5–10 records), confirm cost via `get_credit_pricing` + `get_credit_balance`, then scale — with a second confirmation for the full-list spend.\n- **Deep Search costs more and is slower** — enable it only when the answer requires live info outside stored fields; skip it when the value is derivable from attributes already on the record.\n- **Follow the prompt-authoring standard** (see its section) for anything running at scale: fixed skeleton, `{placeholder}` tokens in the VARIABLES section only, \"Unknown\"/`ESTIMATION` for gaps, refusal prevention, language lock, no marketing-language signals, recall over precision for detection fields, and chained single-decision fields (classification split from reasoning).\n- **Entity is fixed by intent** — a CONTACT field can only reference contact-accessible fields; a COMPANY field, company fields.\n- **Names are unique per entity** — a clashing name returns 409.\n- **`start_an_actions_run` takes exactly one target selector** (`contactIds`, `companyIds`, `contactGroupIds`, or `companyGroupIds`).\n- **Rate limits:** AI-variable writes 30 req/min.\n\n---\n\n## Examples\n\n**Example 1 — Personalized hook fact (text, CONTACT)**\nUser: \"Generate a one-line relevance fact for each contact referencing their role and company.\" → `get_contact_field_metadata` → confirm `{firstName}`, `{jobTitle}`, `{companyName}`, `{industry}` → `create_an_ai_variable` (entity CONTACT, type text) → sample-run `FILL_LEAD_WITH_SMART_FIELDS` on 8 contacts (quote credits, confirm) → review: too generic → `update_an_ai_variable` tightening the prompt → re-sample: good → confirm full-list cost → run on `contactGroupIds` → use `{hook}` in launch-campaign copy or embed via `{aiResearch:<id>}` in an AI Message.\n\n**Example 2 — Company classification (oneOf, COMPANY)**\nUser: \"Tag each company as Enterprise / Mid-Market / SMB.\" → `get_company_field_metadata` → `create_an_ai_variable` (entity COMPANY, type oneOf, values [\"Enterprise\",\"Mid-Market\",\"SMB\"], provideExplanation true, prompt using `{employeeCount}`, `{companyName}`) → sample on 10 companies → review explanations → scale → feed into enrich-and-score-lead.\n\n**Example 3 — Web-researched fact (Deep Search)**\nUser: \"For each company, find their most recent funding round.\" → `create_an_ai_variable` (entity COMPANY, type text, `search: true`, prompt anchored on `{companyName}` + `{website}`) → sample of 5, verify accuracy carefully (research fields hallucinate more) → tighten prompt if needed → scale.\n\n---\n\n## Troubleshooting\n\n| Problem | Fix |\n|---|---|\n| Create/update 400 \"unsupported placeholder\" | A `{placeholder}` isn't a real field — re-check `get_*_field_metadata`; remove `{previousMessage}`-style generics |\n| Create returns 409 | Name already exists for that entity — rename or update the existing one |\n| 400 folder/entity mismatch | Folder belongs to the other entity type — use a matching folder or omit `folderId` |\n| `oneOf` field rejected | Missing the required `values` list |\n| User asks for an \"AI message\"/\"AI snippet\" here | Wrong skill — route to ai-message-builder / ai-snippet-builder (they handle old-vs-new workspace differences) |\n| Sample output is generic/wrong tone | Iterate the prompt via `update_an_ai_variable`, re-sample — don't scale a bad prompt |\n| Research value hallucinates | Ensure `search: true`, anchor the prompt on `{companyName}`/`{website}`, verify samples before scaling |\n| Fill run 400 \"no entities to process\" | The target selector is empty or IDs are invalid — check the list/IDs |\n| Run stuck at PROCESSING with old `lastUpdatedAt` | Worker backlog, not a hang — keep polling `get_actions_run_status`, don't re-trigger |\n| Insufficient credits | `spendableCredits` < cost — report the shortfall; sample smaller or don't run |\n"
}

SHA-256: de6f65c0e26d2f6848f78301210b14967ecce0658610f21008b071c1af825c50