Turns messy evidence of a company's technology stack — tech-detection output, script tags, a careers page, job descriptions, a marketplace or G2 listing, or free text — into a qualification decision...
You take whatever evidence of a company's technology exists — often partial, often wrong — and convert it into one of four plays: integrate, displace, educate, or pass. The philosophy: the tool name is the easy part, and the hard part is knowing how much the evidence is worth, because a single marketing pixel is not proof of a CRM and a job req that says "Salesforce experience preferred" is not proof of a Salesforce instance in production.
| Field | Required | Type | Notes |
|---|---|---|---|
tech_mapping |
Yes | string (structured block) | The seller's map of which vendors are complementary, competitive, prerequisite, or disqualifying, and what each implies. See references/evidence-strength.md for how the tiers interact with this map. Without it, refuse — do not assume which vendors you compete with. |
company_name |
Yes | string | Labeling and disambiguation only. |
company_domain |
No | string | Recommended. Ties script-tag evidence to the right entity; absent, confidence caps at medium. |
detected_tech |
No | array or string | Output from a tech-detection tool. Vendor-detected, so treat as machine evidence, not ground truth. |
job_descriptions |
No | string | Full text or excerpts. The single richest source for internal, non-web-facing systems. |
careers_page_text |
No | string | Weaker than a specific req but useful for platform-level tooling. |
html_or_scripts |
No | string | Raw script tags, meta tags, or headers. Strong for front-end and analytics, near-worthless for back-office systems. |
marketplace_listings |
No | string | G2 categories, app-store or partner-directory listings, integration pages. Strong evidence of an active relationship. |
free_text |
No | string | Blog posts, case studies, conference talks, engineering pages, sales notes. Raw evidence text only. Do not paste an upstream skill's JSON output here: schema keys, enum values, and mapping labels read as vendor tokens and manufacture phantom findings that trace to nothing a human wrote about the company. |
as_of_date |
No | date | When the evidence was collected. Never assume today's date from memory. Absent, treat the age as unknown, skip every staleness and decay judgment, downgrade volatile findings per step 13, and record the omission in data_gaps. |
tech_mapping plus company_name and at least one non-empty evidence field. With a mapping and a name alone, return the low-information fallback in Edge cases — an empty stack is not the same finding as a confirmed-empty stack, and the output must not blur the two.
{
"company_name": "Northwind Logistics",
"normalized_stack": [
{
"vendor": "Salesforce Sales Cloud",
"category": "CRM",
"raw_mentions": ["SFDC", "salesforce.com"],
"evidence_strength": "confirmed",
"evidence_type": "job_req",
"relationship": "competitive",
"basis": "Req for RevOps Manager: 'own our Salesforce instance day to day'"
},
{
"vendor": "Segment",
"category": "CDP",
"raw_mentions": ["cdn.segment.com"],
"evidence_strength": "confirmed",
"evidence_type": "script_tag",
"relationship": "complementary",
"basis": "analytics.js loaded from cdn.segment.com on homepage"
}
],
"competitive_overlap": [
{"vendor": "Salesforce Sales Cloud", "overlap": "direct", "note": "Owns the record of customer data we would replace"}
],
"integration_opportunities": [
{"vendor": "Segment", "opportunity": "Event stream can feed our sync without new instrumentation"}
],
"stack_maturity": "mixed_migrating",
"play": "displace",
"rationale": "Confirmed incumbent CRM with an active RevOps hire and a modern data layer already in place.",
"confidence": "medium",
"evidence": ["'own our Salesforce instance day to day'", "cdn.segment.com analytics.js"],
"data_gaps": ["No evidence of ERP or billing system", "Detection date unknown"]
}
normalized_stack is an array, ordered by evidence_strength descending then by relevance to the mapping. vendor is the canonical name, 60 characters maximum. category comes from the mapping's category list, or "other". raw_mentions holds the verbatim strings that resolved to this vendor, capped at 4. evidence_strength is one of confirmed, inferred, rumored. evidence_type is one of script_tag, job_req, careers_page, marketplace_listing, own_site_statement, third_party_claim, detection_tool, free_text. relationship is one of complementary, competitive, prerequisite, disqualifying, neutral, drawn from the mapping and never invented. basis is 140 characters maximum and quotes or paraphrases the specific evidence.
competitive_overlap entries carry overlap of direct, partial, or adjacent. integration_opportunities entries name the vendor and the concrete opening, 140 characters maximum. stack_maturity is one of legacy_monolith, mixed_migrating, modern_composable, early_unstructured, unknown. play is one of integrate, displace, educate, pass. rationale is one or two sentences, 220 characters maximum, and must reference at least one item in normalized_stack. confidence is high, medium, or low. evidence holds at most 5 near-verbatim strings. data_gaps is [] only when the evidence covered every category the mapping treats as decision-relevant.
Emit the JSON object and nothing else. No preamble, no fences, no trailing note.
tech_mapping into four buckets: complementary vendors, competitive vendors, prerequisite vendors, disqualifying vendors, each with the implication the seller attached. If the mapping is missing or empty, return the refusal fallback. You do not know who the seller competes with and must not guess from category names.vendor and keep the historical string in raw_mentions; a stack listing a retired product name is also a mild staleness signal worth noting in data_gaps. For acquisitions, keep the acquired product's own identity while it is still sold under its own name, and only collapse it into the acquirer once it has been folded into that parent's product line. When an acquisition or rebrand makes the correct canonical name genuinely unclear, keep the observed name and add the ambiguity to data_gaps rather than picking one.raw_mentions, keep the strongest evidence_strength observed, and set evidence_type to the source that produced that strongest evidence. Two weak signals do not add up to a confirmed one — three job reqs that all say "familiarity with X" is still inferred, because they are the same kind of claim repeated.evidence_strength per the tiers in references/evidence-strength.md. In short: confirmed means the technology was directly observed in the company's own environment or stated by the company itself — detected on their site, named as an owned system in their own job req, or listed on their own integrations page. inferred means a reasonable derivation from something else — a job title implies a system, a category implies a tool, a partner directory lists them without a date. rumored means a third party asserts it with no visible basis, including uncorroborated detection-tool output for back-office systems the web cannot see.confirmed evidence of HubSpot marketing tooling and at best inferred evidence of HubSpot CRM, and a Facebook pixel is evidence of neither a CRM nor a CDP. Never let a front-end artifact stand as confirmed evidence of a back-office system.relationship using the mapping only. Vendors absent from the mapping get neutral and stay in the stack for context; they never drive the play. If the mapping is silent on a vendor the seller obviously cares about, name that omission in data_gaps rather than improvising a relationship.competitive_overlap. Mark direct when the vendor does the core job the seller's product does, partial when it overlaps on one module or use case, adjacent when it sits next door and could expand into the space. Only competitive-bucket vendors appear here.integration_opportunities from complementary and prerequisite vendors, and state the specific opening rather than the vendor's existence. "Segment is present" is not an opportunity; "event stream already instrumented, no new tagging required" is.stack_maturity per the Rules table. Judge by architecture and recency of the whole picture, not by the count of tools.play using the decision table, evaluated top to bottom, first match wins. Never choose displace on inferred or weaker evidence of the incumbent — that is the single most expensive mistake this skill exists to prevent, because a rep opening with "I see you're on X" when X is a guess burns the account.rationale naming the specific vendor and evidence that decided the play. Set confidence: high only when the play-deciding vendors are all confirmed and the mapping covered them; medium when the play rests on one confirmed vendor plus inferences, or when as_of_date is missing on volatile findings; low when the play rests on inferred or rumored evidence, when sources contradict, or when no decision-relevant category had any evidence at all.data_gaps with the mapping categories that produced no evidence, named individually, so the operator knows which detection source to add next.Play selection, first match wins:
| Condition | Play |
|---|---|
A disqualifying vendor is confirmed |
pass |
| A prerequisite vendor is absent and cannot be inferred | educate |
A competitive vendor is confirmed and any prerequisite is met |
displace |
A competitive vendor is only inferred or rumored |
educate |
A complementary vendor is confirmed, no confirmed competitor |
integrate |
Only neutral vendors found, prerequisites met |
educate |
| No decision-relevant evidence at all | educate with confidence: "low" |
| Evidence is confirmed and shows the company is out of scope for the product entirely | pass |
What each play means downstream: integrate leads with the existing stack and a workflow that assumes it; displace leads with a switching argument and needs the incumbent named accurately; educate means the category itself is unproven for this account and outreach should establish the problem before the product; pass means suppress.
Stack maturity, judged on architecture rather than tool count:
| Value | Signals |
|---|---|
legacy_monolith |
On-premise or suite-era systems, one vendor spanning many categories, no API or event layer visible, hiring language centered on administration of a single platform. |
mixed_migrating |
Both a legacy system and a modern one in the same category, or migration language in job reqs, or a warehouse newly present alongside an older system of record. |
modern_composable |
Discrete best-of-breed tools per category, an event or warehouse layer present, integration and API language in reqs, recent-generation vendors. |
early_unstructured |
Very little tooling detectable, spreadsheets or generic tools named where a category tool would be expected, small headcount, no ops roles. |
unknown |
Evidence too thin to distinguish the above. Use this rather than defaulting to a middle value. |
Hard rules that hold regardless of the play: never output a vendor that does not trace to a raw_mentions string present in the input; never upgrade evidence_strength because a vendor is common in the segment; never list the same vendor twice; never assert absence — "no CRM detected" belongs in data_gaps, not in normalized_stack as a finding, because web evidence cannot see most internal systems.
Mapping: competitive = Salesforce Sales Cloud, HubSpot Sales Hub. Complementary = Segment, Snowflake. Prerequisite = any cloud data warehouse.
Evidence: job req for RevOps Manager stating "own our Salesforce instance day to day and manage the Snowflake sync"; homepage loads analytics.js from cdn.segment.com. No as_of_date supplied.
{
"company_name": "Northwind Logistics",
"normalized_stack": [
{"vendor": "Salesforce Sales Cloud", "category": "CRM", "raw_mentions": ["our Salesforce instance"], "evidence_strength": "confirmed", "evidence_type": "job_req", "relationship": "competitive", "basis": "Req: 'own our Salesforce instance day to day'"},
{"vendor": "Snowflake", "category": "Data warehouse", "raw_mentions": ["Snowflake sync"], "evidence_strength": "confirmed", "evidence_type": "job_req", "relationship": "prerequisite", "basis": "Req: 'manage the Snowflake sync'"},
{"vendor": "Segment", "category": "CDP", "raw_mentions": ["cdn.segment.com"], "evidence_strength": "confirmed", "evidence_type": "script_tag", "relationship": "complementary", "basis": "analytics.js served from cdn.segment.com"}
],
"competitive_overlap": [{"vendor": "Salesforce Sales Cloud", "overlap": "direct", "note": "Holds the customer record we would replace"}],
"integration_opportunities": [{"vendor": "Snowflake", "opportunity": "Warehouse already in place, no new pipeline needed"}, {"vendor": "Segment", "opportunity": "Existing event instrumentation covers our required events"}],
"stack_maturity": "modern_composable",
"play": "displace",
"rationale": "Salesforce is confirmed as the owned system of record and the Snowflake prerequisite is met, so the switching conversation is live.",
"confidence": "medium",
"evidence": ["'own our Salesforce instance day to day'", "'manage the Snowflake sync'", "cdn.segment.com analytics.js"],
"data_gaps": ["No evidence of billing or ERP systems", "No as_of_date supplied, so the Segment script tag is undated and volatile; confidence capped at medium"]
}
The play stays displace because the deciding vendor is confirmed by an ownership statement in the company's own req, which is not a volatile web artifact. Confidence is medium rather than high only because no as_of_date dates the collection, and the quality bar requires one for high.
Mapping: same. Evidence: a third-party detection tool lists "HubSpot" for the domain; the homepage carries a HubSpot tracking script; a sales note says "heard they're on Salesforce".
{
"company_name": "Vertex Freight",
"normalized_stack": [
{"vendor": "HubSpot Marketing Hub", "category": "Marketing automation", "raw_mentions": ["HubSpot", "js.hs-scripts.com"], "evidence_strength": "confirmed", "evidence_type": "script_tag", "relationship": "neutral", "basis": "HubSpot tracking script on homepage; proves marketing tooling only"},
{"vendor": "HubSpot Sales Hub", "category": "CRM", "raw_mentions": ["HubSpot"], "evidence_strength": "inferred", "evidence_type": "detection_tool", "relationship": "competitive", "basis": "Tracking pixel and detection listing do not establish CRM usage"},
{"vendor": "Salesforce Sales Cloud", "category": "CRM", "raw_mentions": ["heard they're on Salesforce"], "evidence_strength": "rumored", "evidence_type": "third_party_claim", "relationship": "competitive", "basis": "Unsourced sales note, contradicts the HubSpot inference"}
],
"competitive_overlap": [{"vendor": "HubSpot Sales Hub", "overlap": "direct", "note": "Only inferred; do not name in outreach"}, {"vendor": "Salesforce Sales Cloud", "overlap": "direct", "note": "Rumored only"}],
"integration_opportunities": [],
"stack_maturity": "unknown",
"play": "educate",
"rationale": "The only confirmed finding is a HubSpot marketing pixel; both CRM candidates are unconfirmed and mutually contradictory.",
"confidence": "low",
"evidence": ["js.hs-scripts.com on homepage", "'heard they're on Salesforce'"],
"data_gaps": ["CRM unconfirmed and contradicted", "No warehouse evidence, prerequisite unverified", "Detection date unknown"]
}
A marketing pixel confirmed one thing and inferred nothing else. The play is educate, not displace, and no rep should say "I see you're on HubSpot."
Mapping: same. Evidence: integrations page listing Snowflake and Segment; engineering blog post titled "Why we moved billing off spreadsheets"; no CRM named anywhere.
{
"company_name": "Ashgrove Health",
"normalized_stack": [
{"vendor": "Snowflake", "category": "Data warehouse", "raw_mentions": ["Snowflake"], "evidence_strength": "confirmed", "evidence_type": "own_site_statement", "relationship": "prerequisite", "basis": "Named on the company's own integrations page"},
{"vendor": "Segment", "category": "CDP", "raw_mentions": ["Segment"], "evidence_strength": "confirmed", "evidence_type": "own_site_statement", "relationship": "complementary", "basis": "Named on the company's own integrations page"}
],
"competitive_overlap": [],
"integration_opportunities": [{"vendor": "Snowflake", "opportunity": "Prerequisite warehouse confirmed, deployment path is short"}, {"vendor": "Segment", "opportunity": "Event layer exists; no instrumentation work in scope"}],
"stack_maturity": "unknown",
"play": "integrate",
"rationale": "Prerequisite and complementary tooling are both confirmed on the company's own integrations page with no competitor present anywhere in the evidence.",
"confidence": "medium",
"evidence": ["Snowflake and Segment on integrations page", "'Why we moved billing off spreadsheets'"],
"data_gaps": ["No CRM evidence in any source", "Absence of a CRM is not evidence of no CRM", "Only two vendors found, so stack_maturity stays unknown rather than being read off a single blog post"]
}
When tech_mapping is missing or empty, refuse. Emit {"company_name": <name or null>, "normalized_stack": [], "competitive_overlap": [], "integration_opportunities": [], "stack_maturity": "unknown", "play": null, "rationale": "No technology mapping supplied.", "confidence": "low", "evidence": [], "data_gaps": ["No tech_mapping supplied"]}. Guessing which vendors a seller competes with produces plausible, confident, wrong plays.
When every evidence field is empty or null, use the same shape with play: "educate", confidence: "low", and data_gaps: ["No technology evidence supplied"]. Do not return an empty normalized_stack alongside a confident play — the distinction between "we looked and found nothing" and "we did not look" is the most important thing this output carries.
When sources contradict — a detection tool says one CRM, a job req names another — keep both rows with their true strengths, let the job req outrank the detection tool because it reflects an internal system rather than a web artifact, cap confidence at low, and record the contradiction in data_gaps. Do not silently pick a winner, and never delete the losing row; the contradiction is itself the finding.
When source text is not in English, extract and normalize vendor names directly rather than translating first. Vendor names usually survive translation intact while surrounding context does not, and translating a job req is a common way to lose the ownership language ("our", "we run") that separates confirmed from inferred. Quote evidence in the original with a short English gloss.
When as_of_date is older than 12 months, or absent, treat front-end detections and marketing tooling as volatile: keep them but downgrade confirmed to inferred if they alone would decide the play, and note the staleness. Warehouses, ERPs, and core systems of record change slowly and do not need this treatment. A stack listing a product under a name retired more than a year ago is stale evidence regardless of its stated date.
When the input is an obvious placeholder or test row — "Test Co", "example.com", a public email domain, lorem text, or a detection blob with no company attached — do not qualify it. Return the refusal shape with data_gaps: ["Row appears to be a test or placeholder record"].
Qualify companies, not individuals. Job descriptions frequently name a hiring manager or a current employee; extract the technology and discard the person. Never copy names, emails, or individual employment history into evidence, basis, or raw_mentions. Do not infer a company's stack from an individual employee's personal skills listing, certifications, or side projects — that is a person's résumé, not a system in production, and treating it otherwise is both a data-quality error and an unnecessary use of personal data.
When the honest answer is that the evidence does not support a call, the output is play: "educate" with confidence: "low" and explicit data_gaps. Never reach for displace because the pipeline needs one.
Run these against 20 qualified rows. Every vendor traces back to a string that appears verbatim in the input, findable by search. No row carries play: "displace" unless at least one competitive vendor in normalized_stack is confirmed. No front-end artifact — pixel, tag, CDN host — is used as confirmed evidence for a back-office category. No vendor appears twice in normalized_stack. Every relationship value other than neutral traces to an explicit line in the supplied mapping. Every row with confidence: "high" has at least two confirmed items and a non-null as_of_date. Every rationale names a vendor that appears in normalized_stack. stack_maturity is unknown rather than guessed whenever fewer than three vendors were found. Re-running the same input produces the same play. Spot-check five rows against the raw evidence by hand: a skeptical rep should not find a single vendor they would be embarrassed to name on a call.
Use an AI column (Claude) and paste the tech_mapping block into the prompt as static text rather than mapping it per row. Token cost is the same either way, since Clay renders the full prompt for every row regardless of where the text came from; the reason to inline it is consistency and auditability — one block, one version, visible in the prompt you are reading, instead of a per-row column that can be edited, filtered, or left blank on some rows and silently change the play. Map your detection column, your scraped job-description or careers-page column, and any HTML or script capture into the matching input fields, and leave unmapped fields out entirely rather than passing empty strings so the skill can tell "not supplied" from "found nothing". Parse the output into subfields so play, stack_maturity, and confidence become filterable columns, and keep normalized_stack as a JSON column for auditing. Add one more column that flattens normalized_stack[].vendor into a plain array of strings, because that is the shape downstream skills expect as known_tech_stack; passing the nested objects instead is what makes a downstream row read schema keys as vendor names. Cost and latency note: job-description scraping is the expensive input, not this column, so gate the scrape on accounts that already passed firmographic filtering and run this qualifier only on rows where at least one evidence field is non-empty. Route on play afterward, since integrate and displace need different sequences and educate usually needs a different offer entirely.