# 05 Signal Builder

> >

- **Type:** Skill
- **Install:** `agentstack add skill-zevenue-headless-gtm-05-signal-builder`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Zevenue](https://agentstack.voostack.com/s/zevenue)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Zevenue](https://github.com/Zevenue)
- **Source:** https://github.com/Zevenue/headless-gtm/tree/main/skills/05-signal-builder
- **Website:** https://zevenue.com

## Install

```sh
agentstack add skill-zevenue-headless-gtm-05-signal-builder
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Signal Builder - the judgment layer (05)

Signal vendors sell facts ("company X raised", "company Y is hiring"). Scrapers sell
pages. Neither tells you whether a fact is a PQS trigger, a PVP opening, or noise for
this particular client. That call - which finding matters, how much, and what to do
with it - is this skill. It runs on model judgment alone: no API key, no credits spent.

Two ways in:

| You have | Mode |
|---|---|
| One prospect - a URL, pasted content, or a content file | A - Single prospect |
| Chain output - records.jsonl and run folders from 01/02/03/04 | B - Batch over records |

Both modes need `icp_context`, and both produce the same two outputs: a human report
and chain records.

## icp_context and icp_shape

`icp_context` (required): what the client sells, who they sell to, and what pain it
removes. 2-4 sentences is enough. If it's missing, ask once - accept a pasted
description or a file path. A signal only exists relative to this context: a hiring
spree is a 9 for one client and noise for another.

`icp_shape` picks the calibration catalog:

| Shape | Prospects look like | Catalog |
|---|---|---|
| `b2b-saas` | funded companies, title-based orgs, tech stacks, exec moves | `references/signal-types-b2b-saas.md` |
| `vertical-smb` | local or owner-operated - studios, venues, clinics, restaurants, trades | `references/signal-types-vertical-smb.md` |

If the user doesn't pass a shape, infer it from icp_context (end-customers that are
local, Maps-addressable businesses → `vertical-smb`; otherwise `b2b-saas`) and state
which one you picked. Read the catalog before scanning - it carries the per-shape
scoring adjustments, not just examples. For a vertical neither catalog fits, see
`references/calibration-guide.md`.

## Mode A - single prospect

1. Get content. Pasted or file content: use as-is. URL only: if the
   03-firecrawl-research skill is installed, run its standard mode and use the scan
   output; otherwise fetch the high-signal pages directly (homepage, /about, /careers,
   /pricing, /blog) with whatever fetch tool is available. List any page you couldn't
   get - a missing careers page is a coverage gap to report, not a zero signal.
2. Judge (method below) and write the report. If this prospect is part of a chain run,
   also emit a record (Mode B, step 3).

## Mode B - batch over chain records

`scripts/signal_io.py` is stdlib-only Python - no installs. Paths below are relative
to this skill's directory.

1. **Collect** - assemble one evidence bundle per domain from upstream outputs:

   ```bash
   python3 scripts/signal_io.py collect \
     --records path/to/03-run/records.jsonl \
     --records path/to/04-run/records.jsonl \
     --scrape-run path/to/03-run \
     --crustdata-run path/to/04-run \
     --out runs/2026-07-21-acme-batch
   ```

   `--records` takes any chain `records.jsonl` - 01, 02 (Maps), and 04 run
   folders all emit one.

   Every flag except `--out` is optional and repeatable; pass whatever upstream
   artifacts exist. Records merge by domain (upstream fields are kept, never
   overwritten). Output: `inputs/.md` bundles, `records_in.jsonl`,
   `tracker.json`, `meta.json`. Re-running collect is safe - done domains stay done.

   Bundles with no page content and no structured signals are flagged `thin` in the
   tracker. For a thin batch, run 03-firecrawl-research first; for 10 or fewer
   domains it's fine to fetch pages yourself as in Mode A and judge from that.

2. **Judge** each pending domain: read `inputs/.md`, apply the method below,
   write the signal JSON to a temp file, then:

3. **Emit** - validate and append to the chain output:

   ```bash
   python3 scripts/signal_io.py emit --run runs/2026-07-21-acme-batch \
     --domain acme.com --signals-json /tmp/acme-signals.json
   ```

   Emit enforces the record contract (fields, score range, approach enum, non-empty
   sentence and source), merges signals into the upstream record, appends to
   `records.jsonl`, and marks the domain done. On a validation failure it names the
   bad signal and rule - fix the JSON and re-emit. Re-emitting a domain replaces its
   record instead of duplicating it.

4. **Resume / finish**: `python3 scripts/signal_io.py status --run runs/` shows
   done/pending. When all domains are done, write `runs//report.md` (per-domain
   report sections plus segment-level patterns you noticed) and point the user at
   `records.jsonl` - that file is what 06-resolution and the router consume.

For batches over ~100 domains, judge in passes of 25 and re-check score distribution
between passes (see the honesty threshold below) so drift doesn't compound.

## How to judge

Work one domain at a time with the shape catalog open.

1. **Pull candidates from every source.** Page content: careers/jobs, blog/news,
   pricing, integrations, footer tech clues. Structured blocks: funding rounds,
   headcount and department growth, recent hires, job postings. Enrichment beats
   inference - if a structured block says 12 recent hires, don't re-derive headcount
   from prose.
2. **A structured signal is a fact, not a verdict.** "18 recent hires" becomes a
   signal only when the roles map to the pain the client solves. That mapping is the
   judgment. The same goes in reverse: don't discard a boring-looking fact before
   checking it against icp_context.
3. **Score each candidate 1-10** against the anchors below, adjusted by the catalog.
4. **Assign an approach** per signal (table below).
5. **Provenance, every signal.** Web signal: quote the sentence verbatim from the page
   and cite the page URL. Structured signal: state the fact exactly as the data
   supports it (round, amount, date; name, title, start date) and cite
   `vendor:section:domain`, e.g. `crustdata:funding:acme.com`. A finding you can't
   source isn't a signal - drop it or report it as a gap.
6. **Prefer combinations.** Hiring for the role plus competitor friction outranks
   either alone. Write the combination as one signal: lead sentence and source_url
   from the strongest piece of evidence, the rest in `notes`.
7. **Cap at 5 signals, always end with a fallback.** Downstream copy uses the top 2-3;
   a 12-item list is unranked noise. If nothing clears a 3, say so plainly - the
   fallback is the campaign in that case.

## Scoring anchors

| Score | Means | Typical evidence |
|---|---|---|
| 9-10 | Switching intent or acute pain, now | competitor in place plus visible friction; active job post for the exact role the product replaces or augments; public complaint that maps 1:1 to the value prop |
| 7-8 | One strong behavioral signal | leadership hire in the relevant function; fresh funding aimed at the client's area; JD language describing the exact pain; negative reviews in the problem area |
| 5-6 | Problem-space evidence | adjacent tools in the stack; hiring around (not in) the function; growth or expansion straining the relevant process |
| 3-4 | Contextual fit only | right stage, size, or business model - no behavioral evidence |
| 1-2 | ICP match only | fallback territory |

Score honestly - downstream copy calibrates tone on the number, so an inflated 8 reads
as false familiarity in the email. If more than 40% of a batch scores 8+, re-check
those domains against the anchors before emitting more.

## Approach per signal

| Approach | Use when | Email shape |
|---|---|---|
| **PQS** (Pain-Qualified Segment) | the signal names a specific, acute pain you can ask about | "Noticed [signal]. Teams in that spot usually deal with [pain]. Is that you?" |
| **PVP** (Permissionless Value Prop) | you can hand over something genuinely useful tied to the signal before asking for anything | "Put together [artifact] for you - [insight]." |
| **Pain-led** | moderate or contextual signals; every fallback | lead with the most common pain for the profile, ask if it resonates |

Signals scoring 8+ usually carry PQS. Pick PVP only when a real artifact exists to
give (an audit, a teardown, a list), not a hypothetical one. 4 and under: Pain-led.

## Output 1 - the report

One section per judged prospect. Keep this structure so reports stay comparable
across prospects and runs:

```markdown
## Signal Scan: {Company}
**URL:** {url} · **Shape:** {icp_shape}
**Summary:** {1-2 sentences: what they do, their situation, the most interesting finding}

### Signal 1: {Name} (Score: X/10)
**Detected:** {the factual finding}
**Implies:** {what their team is dealing with day to day}
**Approach:** {PQS | PVP | Pain-led}
**Angle:** {one sentence - the core message this signal enables}
**Source:** {url or vendor ref} - "{the quotable sentence}"
**Data points for copy:** {variable}: {value} · {variable}: {value}

### Signal 2: ... (highest score first)

### Fallback (Score: X/10)
**Assumption:** {most common pain for this profile}
**Approach:** Pain-led
**Angle:** {one sentence}
```

Close the report with the handoff: top 1-2 signals plus the fallback are the campaign
set to pass to email-writer (or whatever writes the copy) - signal 1 drives the lead
campaign, the fallback drives the catch-all.

## Output 2 - the chain record

What 06-resolution and the router parse. One JSON object per domain in
`records.jsonl`: the upstream record with two added fields. Upstream fields
(`company`, `domain`, `person`, `filters_matched`, anything else) pass through
untouched.

```json
{"company": "Acme Corp", "domain": "acme.com", "person": null,
 "filters_matched": ["has_homepage", "has_careers", "Series A $18M"],
 "signals": [
   {"signal_type": "hiring-role-replaced",
    "signal_sentence": "You'll own supplier escalations across email, WhatsApp and phone.",
    "source_url": "https://acme.com/careers",
    "score": 9, "approach": "PQS",
    "evidence_date": "2026-07", "notes": "2 open Supply Chain Coordinator roles"}
 ],
 "fallback_approach": {"approach": "Pain-led",
                       "angle": "Mid-market distributors usually drown in manual PO chasing"}}
```

Field rules (emit enforces them):

- `signal_type` - kebab-case; use the catalog's type name where one fits, free-form otherwise
- `signal_sentence` - the referenceable evidence: verbatim quote for web signals, faithful fact statement for structured signals
- `source_url` - `http(s)://` page URL, or `vendor:section:domain` for structured signals
- `score` - integer 1-10 · `approach` - `PQS` | `PVP` | `Pain-led`
- signals sorted by score descending, max 5; `evidence_date` and `notes` optional
- `fallback_approach` - always present: `{approach, angle}`

## Failure modes to avoid

- **Embellished quotes.** The signal_sentence for a web signal is copied from the
  page, not improved. If the finding is a composition ("3 roles in 30 days"), the
  composition goes in the report and `notes`; the sentence stays verbatim.
- **Generic signals.** "They're growing" is not a signal. "Posted 3 Supply Chain
  Coordinator roles in the last 30 days" is.
- **Inflated scores.** A 4/10 batch is useful information - it tells the client this
  segment needs a Pain-led catch-all, not personalization theater.
- **Invented sources.** Never cite a URL you didn't see in the bundle or fetch. If
  coverage was thin, say which pages were missing instead.
- **Editing upstream fields.** The record is additive; you add `signals` and
  `fallback_approach`, nothing else changes.

## Scope boundaries

| Not this skill | Use instead |
|---|---|
| Scrape sites at scale | 03-firecrawl-research |
| Pull funding / headcount / hires from vendors | 04-crustdata-signals |
| Find or verify email addresses | 06-resolution-email-person |
| Write the actual emails | email-writer |

## References

- `references/signal-types-b2b-saas.md` - B2B SaaS calibration catalog
- `references/signal-types-vertical-smb.md` - vertical SMB calibration catalog
- `references/calibration-guide.md` - reply-gym format: calibrate a new vertical from classified reply data

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Zevenue](https://github.com/Zevenue)
- **Source:** [Zevenue/headless-gtm](https://github.com/Zevenue/headless-gtm)
- **License:** MIT
- **Homepage:** https://zevenue.com

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-zevenue-headless-gtm-05-signal-builder
- Seller: https://agentstack.voostack.com/s/zevenue
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
