← EnginyCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Enginy
Snapshot Sep 30, 2026 · 22:51 UTC · version 1.0.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "enrich-and-score-lead",
"description": "Enrich a prospect (email, phone, LinkedIn, company data) and produce an ICP-fit score, single-record or in bulk. Use when the user says \"enrich this lead\", \"fill in the missing contact info\", \"find this person's email and phone\", \"score this contact against our ICP\", \"how well does this prospect fit\", \"grade my list against our ideal customer profile\", or \"enrich and qualify these contacts\".",
"included_files": [],
"skill_md_contents": "---\nname: enrich-and-score-lead\ndescription: >-\n Enrich a prospect (email, phone, LinkedIn, company data) and produce an ICP-fit\n score, single-record or in bulk. Use when the user says \"enrich this lead\",\n \"fill in the missing contact info\", \"find this person's email and phone\",\n \"score this contact against our ICP\", \"how well does this prospect fit\",\n \"grade my list against our ideal customer profile\", or \"enrich and qualify these\n contacts\".\nversion: 1.1.0\n---\n\n# Enrich and Score Lead\n\n## Role and goal\n\nYou take a prospect that exists in Enginy (or that you create) from thin to\ndecision-ready: fill the gaps that block outreach (email, phone, LinkedIn,\ncompany firmographics), then attach an explainable ICP-fit tier so the user\nknows whether to pursue. You run enrichment through billable actions runs, so\nyou never spend credits without showing the cost and getting an explicit yes.\nWorks on one contact or a whole list.\n\n## Instructions\n\n### Phase 1 — Locate (or create) the record\n1. If given a contact ID, call `get_a_single_contact`. Otherwise search with\n `get_contacts` (`search` by name/email) or `search_contacts_with_advanced_filters`\n for anything richer. For bulk, resolve a list ID via `get_lists`.\n2. If the prospect does not exist, create it with `create_a_new_contact`\n (supply `firstName`, `lastName`, and either `linkedInProfileUrl` or\n `companyName` + `professionalEmail` so downstream enrichment has an anchor).\n Merge-on-create is on by default.\n3. Return the `appUrl` (and `companyAppUrl`) so the user can open the record.\n\n### Phase 2 — Gap check\n1. Read the record's current fields. Use `get_contact_field_metadata` /\n `get_company_field_metadata` if you need the exact field names for this\n workspace.\n2. List what is missing and pick only the actions that fill a real gap. Do not\n re-enrich a field that is already populated and verified. Typical gaps →\n actions:\n - No email → `ENRICH_WITH_EMAIL`\n - No phone → `ENRICH_WITH_PHONE`\n - Email present but unverified → `VERIFY_LEAD_EMAIL`\n - Phone present but unverified → `VERIFY_LEAD_PHONE`\n - Has LinkedIn URL, thin profile → `SCRAPE_LEAD_FROM_LINKEDIN`\n - No LinkedIn URL but has name + company → `LINKEDIN_FROM_NAME_LASTNAME_COMPANY`\n - Company record thin → `SCRAPE_COMPANY_FROM_LINKEDIN` or\n `SCRAPE_COMPANY_ACCOUNTIQ_FROM_LINKEDIN` (AI insights)\n\n### Phase 3 — Enrich (billable — confirm first)\n1. **Always** call `get_credit_pricing` and `get_credit_balance` before\n starting. Map each planned action to its pricing key (e.g. `ENRICH_WITH_EMAIL`\n → `ENRICH_LEAD_EMAIL`, `SCRAPE_LEAD_FROM_LINKEDIN` → `SCRAPE_LEAD_LINKEDIN`,\n `SCRAPE_COMPANY_FROM_LINKEDIN` → `SCRAPE_COMPANY_LINKEDIN`). Multiply by the\n number of records. Confirm `spendableCredits >= total cost`.\n2. Show the user the action list and the estimated credit spend, and get an\n explicit go-ahead. Never start a billable run silently.\n3. Start with `start_an_actions_run`. **One target kind per run** — provide\n exactly one of `contactIds`, `companyIds`, `contactGroupIds`, or\n `companyGroupIds`. Contact-side actions (enrich/verify/scrape lead) and\n company-side actions (scrape company) target different kinds, so they need\n **separate runs**. For a waterfall, set `ENRICH_WITH_EMAIL` options\n `stopType` (`VERIFIED_EMAIL` to stop once a verified email is found, `PHONE`,\n or `NONE` to try every provider) and `speed` (`SLOW` = thorough, `FAST`).\n4. Poll `get_actions_run_status` with the returned `actionsId` until\n `overallStatus` is terminal (COMPLETED / FAILED / CANCELLED / PARTIAL). Read\n `statusCounts` for per-record outcomes — `alreadyUpToDate` means the worker\n skipped a current record; `blocklisted` means the target is on the workspace\n blocklist. If it stalls at PROCESSING with an old `lastUpdatedAt`, that is a\n worker backlog, not your problem to retry immediately.\n\n### Phase 4 — Score against ICP\n1. Get the user's ICP definition. If they don't have one, route to the\n `icp-definer` skill first — a good score needs a real profile.\n2. **Prefer nested scoring layers over one mega-variable.** A single \"score\n everything at once\" prompt is hard to trust and hard to debug. Enginy's\n authoring standard is to chain single-decision variables, each doing one job\n and feeding the next by its `{fieldName}`:\n\n ```\n industry_classification (oneOf) → tech_stack_fit (oneOf) → icp_tier (oneOf)\n ```\n\n Each layer is independently testable and filterable, and a wrong tier is easy\n to localize to the layer that caused it. For a simple ICP a single `icp_tier`\n variable is still fine — reach for the chain when the profile has multiple\n independent dimensions (industry AND size AND tech AND signal).\n3. **Name variables in lowercase `snake_case` scoped to their job** —\n `icp_tier`, `tech_stack_fit`, `funding_recency`, `industry_classification`.\n Scoped names keep a growing set of scoring fields legible and make the chain\n self-documenting.\n4. **Split the classification from its reasoning.** For each layer keep the\n filterable label and its explanation as one variable via `type: oneOf` +\n `outputSchema.provideExplanation: true` (the explanation rides alongside the\n `oneOf` value, so you can still filter cleanly on the label while keeping the\n reason for QA). Create each with `create_an_ai_variable`:\n - `entity: CONTACT` (or `COMPANY` if scoring accounts)\n - `type: oneOf`, `values: [\"A\",\"B\",\"C\"]` (or Tier 1/2/3 — whatever the user\n wants), `provideExplanation: true`.\n - `prompt` built from the user's ICP criteria; a downstream layer references\n upstream layers by their `{fieldName}`. **Placeholders must be real field\n names** — pull them from `get_contact_field_metadata` /\n `get_company_field_metadata`. Do not invent placeholders.\n5. **For detection/matching layers, bias toward recall over precision.** When a\n layer's job is to *detect* a trait or *match* a signal (does this company do\n X, does it fit segment Y), instruct the prompt to over-include rather than\n miss a valid prospect — \"when uncertain, include rather than exclude.\" A\n missed prospect is gone for good; a false positive is caught by the\n downstream `icp_tier` layer that filters it out. Precision is the final\n tier's job, not the detector's.\n6. Run each layer with `start_an_actions_run` → `FILL_LEAD_WITH_SMART_FIELDS`\n (or `FILL_COMPANY_WITH_SMART_FIELDS`), passing the variable name(s) in\n `options.fields` (run an upstream layer before the layer that depends on it).\n This is billable — pricing key `FILL_LEAD_WITH_SMART_FIELDS_AVERAGE`; confirm\n spend as in Phase 3. Poll to completion.\n7. Read back the tier and explanation from the record.\n\n### Phase 5 — Act on the score\n- **High fit** → offer to route into `build-targeted-lead-list` (group the\n A-tier records) and `launch-campaign`.\n- **Poor fit** → surface the explanation. If the user agrees it is a hard miss,\n optionally suppress it: `add_blocklist_entries_by_value` with\n `reason: NOT_TARGET` and the right `type` (`EMAIL`, `LINKEDIN_URL`, `DOMAIN`,\n or `COMPANY_LINKEDIN_URL`). Confirm before blocklisting — it excludes the\n target from future runs.\n\n## Enginy MCP tools used\n\nget_a_single_contact, get_contacts, search_contacts_with_advanced_filters,\ncreate_a_new_contact, get_lists, get_contact_field_metadata,\nget_company_field_metadata, get_credit_pricing, get_credit_balance,\nstart_an_actions_run, get_actions_run_status, create_an_ai_variable,\nadd_blocklist_entries_by_value\n\n## Important notes\n\n- **Confirm before spend, every time.** Enrichment, verification, LinkedIn\n scraping, and running AI variables are all billable. Always\n `get_credit_pricing` + `get_credit_balance` and get an explicit yes before\n `start_an_actions_run`. `EXPORT_TO_CRM` / CRM sync have no credit-mapped entry.\n- **One target kind per actions run.** You cannot mix contact-only and\n company-only actions in a single run. Split them.\n- **Enrichment is best-effort.** A provider waterfall can return nothing;\n `statusCounts` will show `failed` for records where no data was found. Enriching\n does not guarantee an email or phone exists.\n- **`oneOf` AI variables require a `values` array.** Set\n `provideExplanation: true` so scores are auditable.\n- **Chain single-decision layers for multi-dimensional ICPs.** Prefer\n `industry_classification → tech_stack_fit → icp_tier` (each `snake_case`, each\n one decision) over one all-in-one prompt — each layer is testable and\n filterable on its own. Bias detection/matching layers toward recall\n (over-include); let the final tier layer supply precision.\n- **Placeholders only from field metadata.** Generic aliases like\n `previousMessage` are rejected by `create_an_ai_variable`.\n- **Verify needs existing data.** `VERIFY_LEAD_EMAIL` / `VERIFY_LEAD_PHONE` only\n work on a contact that already has a stored email / phone.\n- UI walkthroughs live at https://docs.enginy.ai — link there rather than\n describing screens.\n\n## Examples\n\n**1. Single thin inbound lead.** User pastes a contact ID with only a name and\ncompany. Locate it, find it has no email/phone/LinkedIn. Propose\n`LINKEDIN_FROM_NAME_LASTNAME_COMPANY` → then a contact run with\n`ENRICH_WITH_EMAIL` (`stopType: VERIFIED_EMAIL`) + `ENRICH_WITH_PHONE`, plus a\ncompany run to scrape firmographics. Show ~X credits, confirm, run, poll. Then\ncreate an A/B/C ICP variable, fill it, report \"Tier A — matches your 50-500 SaaS\nICP, VP-level\" with the `appUrl`.\n\n**2. Bulk list qualification.** User wants their 400-contact \"Webinar signups\"\nlist scored. Skip enrichment if records are already complete (check a sample).\nCreate the ICP variable once, run `FILL_LEAD_WITH_SMART_FIELDS` against\n`contactGroupIds: [listId]`, poll, then offer to build an A-tier sublist and\nlaunch a campaign.\n\n**3. Poor-fit cleanup.** A scored contact comes back Tier C — \"solo founder,\nno budget signal, outside target size.\" Show the explanation; on user\nconfirmation, blocklist the domain with `reason: NOT_TARGET`.\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| `start_an_actions_run` 400 \"no entities to process\" | Target selector empty or all records filtered out | Re-check the ID list; confirm the list isn't empty |\n| 400 validation error on the run | Contact + company actions in one run | Split into one run per target kind |\n| Enrichment completes but field still empty | No data found by any provider | Read `statusCounts.failed`; try `speed: SLOW` or a wider `sortedApis` |\n| `create_an_ai_variable` 400 unsupported placeholder | Placeholder not a real workspace field | Re-fetch names via `get_contact_field_metadata` |\n| `create_an_ai_variable` 409 | Variable name already exists for that entity | Reuse it or pick a new name |\n| Score run shows records `blocklisted` | Target already on the blocklist | Expected — those are intentionally excluded |\n| Run stuck at PROCESSING | Worker backlog (stale `lastUpdatedAt`) | Keep polling; do not restart the run |\n| Not enough credits | `spendableCredits` < cost | Reduce record count or top up before running |\n"
}SHA-256: 0c62de485b1d5fe0260cd09e42cbbbf575eb7cfd41d3fb36657f4c2f2e34ff7f