Deepline Ads Audiences
SkillDev toolsUse this skill when building, enriching, auditing, or uploading B2B paid ads audiences to Google Customer Match, Meta/Facebook Custom Audiences, or LinkedIn Matched Audiences. Triggers on phrases like '/deepline-ads-audience', '/deepline-ads-audiences', 'upload this audience', 'create custom audiences', 'personal email hashes', 'increase Facebook match rate', 'Google ads audience', 'Meta audience', 'FB audience', or any workflow that turns CRM/customer/contact data into paid ads upload lists. Skip for outbound prospecting sequences, cold email, or pure campaign copywriting.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Deepline Ads Audiences skill
What this skill tells your AI
The instructions your AI receives, as published by getaero-io/gtm-eng-skills in skills/deepline-ads-audiences/SKILL.md and read by ahel’s review.
Quick Start
npm install -g deepline
# Fallback for secure sandboxes: mkdir -p "$HOME/.local" && npm config set prefix "$HOME/.local" && export PATH="$HOME/.local/bin:$PATH" && npm install -g deepline --registry https://code.deepline.com/api/v2/npm/
deepline auth register --wait auto
deepline auth wait --timeout 120 # completes Cowork/browser approval; no-op if already connected
deepline auth status
deepline -h
CLI resolution
Run deepline when it is available. If the shell reports that command is missing, use <workspace-root>/.deepline/runtime/bin/deepline (or the npm-created .cmd shim on Windows). If neither exists, follow https://code.deepline.com/INSTALL.md to set up Deepline.
Build high-quality ABM paid ads audiences from first-party customer or prospect lists. This skill is for paid ads audience upload and evaluation, not outbound.
Names in this skill are starting hints. Run deepline tools search audience --json and deepline tools describe <tool_id> --json before executing because tool names and payload shapes can change. Tool search accepts an intent query or, for structured filtering, --categories and/or --search_terms; a filter-only search needs at least one of those flags. Use commas for multiple filter values, and put provider names in the query rather than using a --prefix flag.
Before You Start
Use the full recipe when the user asks to enrich and upload audiences to Facebook/Meta and Google:
→ Read recipes/enrich-and-upload-facebook-google.md.
Use the max-coverage recipe when the user asks for "max coverage", "maximum match rate", "keep increasing coverage", "get to 75% coverage", or asks to exhaust LinkedIn/personal-email/hash options:
→ Read recipes/max-coverage-audience.md.
This skill is not for cold outbound, sequencing, or copywriting. Personal emails here are used to improve paid ads matching, not to contact people directly.
Decision Matrix
| User says | Do this | Read |
|---|---|---|
| "max coverage", "highest match rate", "keep increasing coverage" | Run the explicit max-coverage ladder with budget gates. | recipes/max-coverage-audience.md |
/deepline-ads-audience, "enrich and upload to FB/Google" | Run the full paid ads audience recipe. | recipes/enrich-and-upload-facebook-google.md |
| "sample ABM segment", "do the example workflow" | Follow the reusable high-priority ABM segment recipe. | recipes/sample-abm-segment-example.md |
| "use ContactOut hashes", "hashed identifiers", "LinkedIn URLs to hashes" | Plan a bulk pass beside the ladder, not a waterfall step. | shared/contactout-hash-pool.md |
| "what is a hash", "why is my match rate low", first-time user | Explain the mechanic before quoting a plan. | shared/audience-basics.md |
encoded/internal-identifier LinkedIn URLs (/in/ACwAA…), "API rejected my LinkedIn URLs", "convert LinkedIn URLs" | Normalize person_linkedin_url before upload: drop encoded, recover vanity. | Step 4 → "Normalize LinkedIn URLs" (this file) |
| "Make sure hashes are not double hashed" | Run the no-double-hash audit play before upload. | plays/audit-no-double-hash.play.ts |
| "enrich this list", "buy personal emails/hashes", "run the ladder" | Run the waterfall. Each layer only sees rows still missing a hash. | plays/enrich-audience-waterfall.play.ts |
| "Compare enriched versus unenriched" | Build both hash-only datasets and report lift. | plays/enrich-audience-waterfall.play.ts |
| "include phone numbers", "add phones" | Hash existing phones digits-only with country code. | shared/upload-failure-modes.md |
| "what was the match rate", "did it match" | Read contactIdInfo.matchRatePercentage, not the range enum. | shared/upload-failure-modes.md |
| "put it in a sheet", "customer will upload" | Publish the validated file to Sheets; verify by row count. | shared/upload-failure-modes.md |
| "upload keeps failing", "422", "audience is locked" | Meta locks on write. Send the audience in one call. | shared/upload-failure-modes.md |
| "Upload to Google" | Validate hash-only rows, create Google audience, sync, readback. | plays/upload-google-hash-only-audience.play.ts |
| "Upload to Facebook and Google", "upload to FB/Google", "Meta + GAds" | Validate once, then upload to Google and Meta. | plays/upload-facebook-google-hash-only-audience.play.ts |
Default Workflow
- Confirm rights, use case, and geography.
- Discover uploadable ad accounts.
- Build baseline and enriched audience objects.
- Validate identifiers and expected match-rate lift.
- Create separate platform audiences.
- Upload rows.
- Check status and report IDs, uploaded counts, invalid rows, and current build state.
Ask about suppression before targeting
Every audience run has a second list hiding in it: the people who should never see the ad. Current customers, closed-lost accounts, active opportunities, employees, and recent converters.
Ask for it explicitly, because users rarely volunteer it and the failure is invisible. A suppression list that was never built, or that silently failed to sync, spends budget advertising to people who already bought.
The risk is asymmetric, which decides how to handle edge cases: an extra person on a suppression list costs a few unserved impressions, while a missing one costs real money and can annoy a customer. When you are unsure whether someone belongs on it, include them.
Suppression lists match on the same identifiers as targeting lists, so a work-email-only suppression list suppresses almost nobody. Enrich it with the same ladder, or it will not do its job.
Expect different lift per platform
Enrichment does not pay off evenly. Meta gains the most from personal-email enrichment, because personal addresses are what people register with there. Google gains less, since a Workspace address is already a Google account and often matches from the baseline.
Set that expectation before spending. A user who was promised uniform lift reads a modest Google result as a failed run, when it is the expected shape.
Explain the shape before you spend
Users new to paid ads usually have not met hashed identifiers before, and a plan that opens with provider names reads as an opaque menu. Before running the ladder, state the mechanic in one or two sentences so the user can judge the plan rather than approve it blindly:
Ad platforms can only match people on identifiers those people gave the platform. Your CRM holds work emails; almost nobody signs up to Meta with a work address. These layers buy the personal identifiers that do match, cheapest first.
Say what each layer costs in Deepline credits and what it is expected to add, then ask for approval before the first paid layer. A user who understands the mechanic will make a better call on where to stop, which is the only decision that controls spend here.
shared/audience-basics.md holds the longer explanation, including what a hash is and why it is safe to send. Point the user there when they ask, or when the run is their first.
Coverage Modes
Choose the coverage mode before spending credits. Record it in the run notes.
| Mode | Use when | Waterfall | Stop condition |
|---|---|---|---|
cost_effective | User asks for the default, low-cost, or first-pass enrichment. | Work-email baseline → Aviato personal hashes on all eligible rows → LimaData personal hashes on remaining personal-hash misses. Optionally a ContactOut bulk pass over rows that still lack a personal hash and have a LinkedIn URL, which runs beside the ladder rather than inside it. | Stop after the hash providers, report contacts still missing personal hashes, then ask before expanded fallback. |
max_coverage | User asks for highest match rate, max coverage, or to keep increasing coverage. | Work-email baseline → phone hashes already present → LinkedIn repair → Aviato personal hashes for all eligible rows → LimaData personal hashes → ContactOut bulk pass beside the ladder → raw personal-email waterfall → platform upload variants. | Stop when no approved provider remains, budget cap is hit, marginal lift is below threshold, or rights/geo constraints block more enrichment. |
Never silently downgrade a max_coverage request to cost_effective. If a provider or credential is unavailable, report the gap and continue with the next approved provider rather than stopping early.
Shareable Plays
This skill includes copyable play templates under plays/. Use them when the user asks for a repeatable or shareable workflow, not just a one-off CLI run.
Before running a template, check the installed surface when it is unclear:
deepline --help
deepline plays --help
Use deepline plays for the bundled templates. If deepline plays is unavailable, stop and ask for the Deepline SDK CLI to be installed or updated instead of approximating the upload through older command paths.
| Play | Purpose | Input |
|---|---|---|
plays/build-hash-only-audience.play.ts | Build baseline and enriched hash-only datasets from source CSV rows. Raw emails are normalized and hashed once. Provider hashes pass through as lowercase hex. | { "file": "input.csv" } |
plays/audit-no-double-hash.play.ts | Verify the final upload payload is hash-only, deduped, populated, includes provider hashes as-is, and does not contain hash-of-hash mistakes. | { "payloadFile": "upload.csv", "providerHashFile": "provider-hashes.csv", "providerHashColumns": ["aviato_hash", "limadata_hash"] } |
plays/build-contactout-hash-pool.play.ts | Batch LinkedIn URLs through ContactOut hashed identifiers into a deduped hash pool. Reports matched profiles, net-new hashes, and per-chunk results. | { "file": "contacts.csv", "limit": 100 } |
plays/upload-google-hash-only-audience.play.ts | Create a Google Customer Match list, upload hash-only rows, and read status back. | { "file": "upload.csv", "account_id": "1234567890", "audience_name": "Segment enriched 2026-06-09" } |
plays/upload-facebook-google-hash-only-audience.play.ts | Upload the same validated hash-only rows to Google and an existing Meta/Facebook Custom Audience. | { "file": "upload.csv", "google_account_id": "1234567890", "meta_ad_account_id": "act_123", "meta_audience_id": "456", "audience_name": "Segment enriched 2026-06-09" } |
plays/report-google-coverage-lift.play.ts | After Google match rates populate, calculate coverage lift, estimated matched identifiers, spend efficiency, and a follow-up note. | { "account_name": "Customer Google Ads", "account_id": "1234567890", "baseline": {...}, "comparisons": [...] } |
Recommended sequence:
deepline plays check ./plays/build-hash-only-audience.play.ts
deepline plays run --file ./plays/build-hash-only-audience.play.ts --input '{"file":"source.csv"}' --watch
deepline plays check ./plays/audit-no-double-hash.play.ts
deepline plays run --file ./plays/audit-no-double-hash.play.ts --input '{"payloadFile":"enriched_hash_only.csv","providerHashFile":"provider_hashes.csv","providerHashColumns":["aviato_hash","limadata_hash"]}' --watch
deepline plays check ./plays/upload-google-hash-only-audience.play.ts
deepline plays run --file ./plays/upload-google-hash-only-audience.play.ts --input '{"file":"enriched_hash_only.csv","account_id":"1234567890","audience_name":"ABM enriched hash-only 2026-06-09"}' --watch
deepline plays check ./plays/upload-facebook-google-hash-only-audience.play.ts
deepline plays run --file ./plays/upload-facebook-google-hash-only-audience.play.ts --input '{"file":"enriched_hash_only.csv","audience_name":"ABM enriched hash-only 2026-06-09","google_account_id":"1234567890","meta_ad_account_id":"act_123","meta_audience_id":"456"}' --watch
deepline plays check ./plays/report-google-coverage-lift.play.ts
deepline plays run --file ./plays/report-google-coverage-lift.play.ts --input '{"account_name":"Customer Google Ads","account_id":"1234567890","segment_name":"High-priority target-account audience","source_rows":20000,"baseline":{"label":"L1 work hash-only","audience_id":"1111111111","match_rate_pct":23,"uploaded_rows":13935},"comparisons":[{"label":"L2 Lima+Aviato hash-only","audience_id":"2222222222","match_rate_pct":35,"uploaded_rows":18386,"deepline_spend_usd":51.47},{"label":"L3 all hashes only","audience_id":"3333333333","match_rate_pct":43,"uploaded_rows":24787},{"label":"L4 all hashes + details","audience_id":"4444444444","match_rate_pct":42,"uploaded_rows":24787},{"label":"L5 LeadMagic top100 fallback","audience_id":"5555555555","match_rate_pct":44,"uploaded_rows":17016},{"label":"L6 GTM LinkedIn + Lima/Aviato","audience_id":"6666666666","match_rate_pct":45,"uploaded_rows":17064}],"spend":{"low_cost_hash_usd":51.47,"contact_fallback_usd":218.37,"total_usd":269.84},"recommendation_label":"L6 GTM LinkedIn + Lima/Aviato"}' --watch
Export dataset outputs after a run with:
deepline runs export <run-id> --out audience-output.csv
Before using the upload play, run account discovery from Step 2 and confirm the selected Google Ads account name and ID with the user.
Step 1: Confirm Rights
Ask for explicit confirmation when the source data belongs to a customer workspace or third party. The minimum confirmation is:
- The source list can be used for paid ads audience creation.
- Any enrichment identifiers can be used for paid ads matching.
- The requested platforms are allowed for this use case.
- Geography is in scope. Default to US-only when personal identifiers are being enriched unless the user specifies otherwise and confirms compliance.
Do not use this skill for outbound email, phone, or sequencing. The output is audience upload data and platform audience IDs.
Step 2: Discover Uploadable Accounts
Run account discovery before every live upload. Agents often guess account IDs from prior context, app IDs, or UI labels. That creates audiences in the wrong account or fails after enrichment spend has already happened.
Use this discovery ladder:
- Search for live account tools:
deepline tools search "ads audience account discovery google meta linkedin" --json
deepline tools list | grep -Ei "account|audience"
-
If a platform exposes a direct account discovery tool or endpoint, use it first. Record account name, account ID, platform, permission status, and whether customer list upload is supported.
-
If no direct discovery tool is exposed, ask the user for the account ID and name, then validate it before upload:
deepline tools execute google_ads_audiences_list_audiences --payload '{"account_id":"1234567890","page_size":10}' --json
deepline tools execute meta_audiences_list_audiences --payload '{"ad_account_id":"1234567890"}' --json
deepline tools execute linkedin_ads_audiences_list_audiences --payload '{"account_id":"urn:li:sponsoredAccount:123456789"}' --json
-
Show the discovered choices back to the user as
Account Name (Account ID), grouped by platform. If there is more than one plausible account, ask which one to use before creating audiences. -
Keep the selected account IDs in the run notes and final answer. A Meta app ID is not an ad account ID; the two look similar enough that agents substitute one for the other, and the upload then fails or lands in the wrong account after enrichment has already been paid for. Meta upload IDs look like
act_123...or a numeric ad account ID that Deepline can prefix.
Step 3: Build Baseline and Enriched Objects
Create two separate objects when evaluating lift:
unenriched: first-party source identifiers only, usually work email plus name, company, country, postal code, and LinkedIn URL context.enriched: source identifiers plus paid-ads-safe enrichment. Prefer hashed personal email providers first, then raw personal-email providers that can be normalized and hashed locally.
Run this as a waterfall, not a fan-out. Every layer below runs only on rows that still have no usable hash. Sending the same row to several providers costs several times over for one identifier, and it hides: every call returns 200, so the run reads as healthy while the bill multiplies. plays/enrich-audience-waterfall.play.ts enforces the skipping and reports attempted, hits, and skipped per layer, so a fan-out is visible in the output.
Default personal-email waterfall for B2B paid ads:
- Baseline first-party identifiers: valid work emails, names, company, country, postal code, LinkedIn URLs, and stable external IDs.
- Aviato
aviato_pull_email_hash: run on all eligible rows with enough identity context, including rows that already have work emails. Use it when the goal is ad upload and the provider returns paid-ads-ready personal email hashes. If the output cell is a JSON object, extract the scalar hash frommatched_result,result.data.hashedEmails[0],result.data.hashed_email, or equivalent hash fields. Do not treat the JSON object string as the upload value. - LimaData
limadata_find_audience_identifiers: run on rows still missing a personal hash after Aviato, or run it first when the user asks for the most cost-effective expansion pass. Extract only normalized 64-character SHA-256 hashes frommatched_result,result.data.hashed_emails[].normalized_hash,hash, orsha256fields. ContactOut hashed identifiers do not belong in this numbered list, because they cannot waterfall. Run them as a separate bulk pass. See the section below.
ContactOut hashed identifiers (quick reference)
ContactOut converts LinkedIn URLs straight into hashed emails, but it does not waterfall: the response is an unattributed pool, so it cannot skip rows another provider covered and later providers cannot skip rows it covered. Run it as a bulk pass beside the ladder.
- Send set: rows with a verified LinkedIn URL. Exclude rows that already have a hash for
cost_effective, include them formax_coverage. - Batch 5 to 100 per call. A zero-match chunk returns HTTP 404 and is not billed.
- Bill and report from
matches_found, never the hash-list length. - Merge into the audience-level hash pool, not per-row
email_sha256cells.
→ Read shared/contactout-hash-pool.md before planning or running a pass. It covers why attribution cannot be recovered, the measured overlap and multi-address rates, and the Meta result.
A work email is not a personal hash
Scope the waterfall on whether a row has a usable personal identifier, not on whether it has any email. A work-email-only contact must run through every layer; a work email is the L0 baseline.
The same mistake hides inside a personal_email column. In one run 151 rows held
a corporate address there, and each one exempted a contact from enrichment.
Re-running only those rows hit 64.9%, against 27.5% on the main pass, for 4.23
USD. Check the domain, not the column name.
Order the ladder by what a miss costs
Two providers at similar prices are not equivalent, because they bill differently on a miss:
| Billing | Providers | Consequence |
|---|---|---|
| Per call, hit or miss | LimaData, Aviato | Every attempted row costs the same |
| Per result or match | LeadMagic, ContactOut | Misses are free, so they suit a thin remainder |
Measured on one 5,549-row list, cheapest first:
| Layer | Attempted | Hit rate | Spend |
|---|---|---|---|
| LimaData | 1,775 | 27.5% | 49.70 USD |
| LimaData, corporate-personal redo | 151 | 64.9% | 4.23 USD |
| ContactOut bulk | 1,772 | 53.0% | 52.64 USD |
| LeadMagic | 1,285 | 5.4% | 4.76 USD |
Run the cheapest per-call provider first so it absorbs the easy hits. A low hit rate on the remainder means the pool is exhausted: LeadMagic cost 4.76 USD to establish that, where a per-call provider bills the same for the same answer.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 58
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
deepline-ads-audiences- Source
- github.com/getaero-io/gtm-eng-skills