deepline-gtm
GTM prospecting, enrichment, outreach, and plays. Discovery: deepline-pre-research. Providers: adyntel,affinity,ai_ark,allegrow,amplemarket,apify,attention,attio,aviato,bettercontact,bigquery,bloomberry,bluesky,bounceban,browserbase,builtwith,clickhouse,cloudflare,contactout,cont
Install
npx skills add https://github.com/getaero-io/gtm-eng-skills/tree/main/skills/deepline-gtm
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install getaero-io-gtm-eng-skills@llmmart
git clone https://github.com/getaero-io/gtm-eng-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole getaero-io/gtm-eng-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
GTM Meta Skill
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 commands bare — no pipes, redirection, 2>&1, command chaining, or backgrounding around them. The CLI already formats, truncates, and prints what you need; deepline billing usage | head reads as parsing and loses output.
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.
Before the first Deepline fanout in a task, run deepline preflight --json as
one standalone command and wait for it to finish. Never submit preflight beside
another Deepline command. It combines health, authentication, and balance in
one process and gives any automatic CLI update a serial boundary.
After preflight succeeds, prefix every Deepline command that may run
concurrently with DEEPLINE_SKIP_SELF_UPDATE=1. This environment prefix is the
only exception to the bare-command rule above. Serial commands may stay bare;
the opt-out is required for every member of a parallel batch.
Debug every Play run first. Start a new run with
deepline plays run <play> --input '<json>' --debug; for an existing run save
the complete retained stream with deepline runs logs <run-id> --out run.log --json, then use
deepline runs get <run-id> --full --json. This preserves the durable trail
instead of spending on a duplicate run. A caught non-2xx ctx.fetch records a
customer-safe diagnostic with its call key, method, destination origin, and
HTTP status. Generic HTTP is a separate provider surface: inspect the full run
package before treating its provider-level error status as the upstream HTTP
status or its error body as a safe customer-facing explanation.
Ask for requirements, not implementation instructions. Requirements are the business outcome, target population, constraints, time horizon, requested destination, and any stated spend or authority boundary. The provider, tool, query shape, identifier recovery, filter expression, fallback order, and workflow structure are implementation decisions. Infer and execute the latter; do not turn them into a questionnaire. When a requirement is genuinely absent and materially changes the result or external action, ask one concise question with a recommendation. Otherwise state a reasonable assumption in the result and keep moving.
Decision-ready communication. The user should never have to infer the
answer from a status update. When work produces people, companies, events, or
rows that determine the next move, show those real records in a readable
Markdown table first. Link a person's name to their verified LinkedIn profile
when one was returned; do not hide the decision behind counts, summaries, or a
generic Profile column. For choices, show the comparison; for copy, show the
draft; for research, show the evidence that supports the conclusion.
Then make one plain-language recommendation based on what is visible. State the concrete boundary—who is in, out, or what changes—not a label such as “keep” or “refine.” Do not ask the user to design routine filters or choose plumbing. Keep validation, raw ids, feeds, and tool mechanics internal unless they change scope, cost, risk, confidence, or action.
When the user is calibrating, choosing a scope, prioritizing a list, or authorizing a change, use this complete envelope:
<the table, comparison, draft, or evidence>
Recommendation: <one concrete next state and why>.
Want me to use that, or adjust it?
End with Want me to use that, or adjust it? exactly. The user can say “yes”
or name the adjustment. A calibration, scope, or prioritization response is
incomplete without that final line, even when it starts no external action. Do
not add a second question, a menu, or an implementation checklist. If no user
decision is needed, state the outcome and stop.
Paid monitors. Before deployment show the recommended scope, live Deepline price, and delivery in the smallest useful shape. Check Slack first; recommend a real connected channel when one is available, otherwise offer Slack or the configured CRM. The monitor recipe covers consent, history, and similar-company scope.
Discovery order: companies first, then people. When the task requires finding contacts at companies matching criteria (portfolio, ICP, hiring signal), discover the company set first, then find people at each company. Do not start with broad people-search queries.
Named companies are enough to start. When the user gives company names but not domains, resolve each canonical company domain before asking them for anything. A domain is a recoverable identifier, not clarification debt. Carry the resolved domain and its official-page evidence into the downstream lookup; do not ask the user to paste a domain list merely because a later tool needs one. Read finding-companies-and-contacts.md for the identity gate and ambiguity handling.
Known companies + nuanced roles: qualify the real title roster first. For requests such as "AI leadership at Mount Sinai," "job titles at these companies," or "find the RevOps buyers at these accounts," read and follow recipes/find-qualified-titles.md: company_titles -> qualify exact roster titles -> deepline_native_search_contact with title_lists. Use Exa afterward for public-profile gaps and DropLeads last for supplemental database rows. Broad audience sizing remains a valid DropLeads use case.
CLI recovery
The SDK CLI is the supported CLI. If it is unavailable, run deepline update
or reinstall it with npm install -g deepline@latest; do not switch CLI families.
2) Read the matching workflow before execution
SKILL.md routes; the matching doc supplies the execution contract. Read it before using a provider so its schema, sequencing, and known failure modes govern the run.
Routing rules — match your task to a doc and READ IT:
| When the task involves... | You MUST read this doc first | What it gives you (that SKILL.md doesn't) |
|---|---|---|
| Finding companies, finding people, building lead lists, prospecting, portfolio/VC sourcing, contact finding at known companies, coverage completion at scale | finding-companies-and-contacts.md | Provider filter schemas, parallel execution patterns, provider mix tables, role-based search rules, subagent orchestration, at-scale coverage completion, portfolio/VC shortcuts, contact finding patterns. |
Researching companies or people, understanding what they build, figuring out use cases, personalizing based on mission/product/industry, enriching a CSV, adding data columns, waterfall enrichment, finding emails/phones/LinkedIn, coalescing data, custom signals, run_javascript / deeplineagent columns, Apify actors — any task that adds or transforms row-level data |
enriching-and-researching.md | Play routing per scenario (deepline plays run + batch prebuilts + fork/wrap), waterfall orders for email/phone/LinkedIn, run_javascript / deeplineagent routing inside custom plays, multi-pass pipeline patterns, coalescing, custom signal buckets, Apify actor selection, GTM definitions and defaults. |
Writing, running, auditing, or modernizing Deepline plays — composing tools/plays, mapping CSV rows, reviewing an existing .play.ts or saved Play for deprecated APIs/tool IDs, fallback logic, joins/projections, durable datasets, custom run/export behavior, or webhook/cron-style orchestration. |
recipes/deepline-plays.md | Direct vs compose decisions, plays check-driven audits and remediation order, bootstrap/wrap/fork rules, durable authoring basics, run/export/repair routing, and exact SDK/API reference pointers. |
| Writing cold emails, personalizing outreach, lead scoring, qualification, sequence design, campaign copy, inspecting CSVs in Playground. If the task also requires researching companies/people to inform the writing, read enriching-and-researching.md too — it has the multi-pass pipeline pattern. | writing-outreach.md | Prompt templates from prompts.json. Scoring rubrics. Email length/tone/structure rules. Personalization patterns. Qualification frameworks. Playground inspection commands. |
Deepline Monitors — continuously capturing a provider's webhook events (email replies, new job postings, intent signals) into a Customer DB table, or deploying/listing/managing those upstream provider pipes. Event-driven streaming, NOT an on-demand enrich/sourcing run. Conditional gate: run deepline monitors status --json first. Read the recipe only when the command exits 0 with has_access: true. Exit 1 with has_access: false means rollout access is absent. For exit 3, fix auth/permission; for exit 5, diagnose configuration/server reachability. Do not reinterpret other failures as rollout denial. |
recipes/deepline-monitors.md | What Monitors are, when to use them vs plays, the command set for delivery health, audit/repair, single monitors, and Fleets; the current billing-recovery boundary; monitor definition shape; the provider-webhook → Customer DB → triggered-play data flow; and access gating. |
If you are hand-authoring enrich columns instead of using a native play, jump straight to the "Handmade step shape quick reference" section in enriching-and-researching.md. That section spells out the exact runtime contract for run_javascript, extract_js, result, and persisted matched_result.
Recipes: step-by-step playbooks for specific tasks (check before executing)
Read the matching recipe before executing. Follow its sequence; adapt it only when the request requires it.
| Recipe | Use when... |
|---|---|
account-orgchart.md |
Building an org chart, account map, buying committee, stakeholder map, or multi-threading plan around a target person or company |
build-tam.md |
Building a total addressable market list or large company list from ICP criteria |
clay-to-deepline.md |
Converting a Clay table to Deepline (deprecated enrich-era recipe — use its action mappings, author the result as a custom play) |
deepline-monitors.md |
ACCESS-GATED. Deepline Monitors continuously capture a provider's webhook events into a Customer DB table and trigger plays. Run deepline monitors status --json first; only exit 1 with has_access: false is a clean rollout denial. Diagnose auth, configuration, and server failures by their actual exit code. |
deepline-plays.md |
Creating or auditing .play.ts and saved Plays, including deprecated APIs/tool IDs, durable datasets, fallback logic, joins/projections, webhook/cron-style orchestration, and custom run/export behavior |
find-qualified-titles.md |
Primary path for nuanced roles at known companies: "AI leadership at Mount Sinai", "find all job titles at these companies", or "find the marketing-ops/RevOps/Salesforce buyers". Pull each company's real title roster (free company_titles), qualify exact titles, then find contacts with tiered (LinkedIn, email, phone) reveal. |
linkedin-url-lookup.md |
Resolving a person's LinkedIn profile URL from their name and company with strict identity validation |
portfolio-prospecting.md |
Finding companies backed by a specific investor or accelerator, then finding contacts and building personalized outbound |
small-business-prospecting.md |
Finding local small businesses or storefront/service-area companies using Maps-style search. Doctors, services business, restaurants, etc. |
Public/social source discovery, community-language pulls, pre-research source planning, or provider-coverage/cost comparison → use the standalone
deepline-researchskill, not a recipe here. It owns X/Twitter, Reddit, Hacker News, Bluesky, and public-registry fanout plus the source-plan + Deepline-cost synthesis.
If none match, grep for more specific keywords: Grep pattern="<keyword>" path="<directory containing this SKILL.md>/recipes/" glob="*.md" output_mode="files_with_matches"
Data
- When the user hands you a CSV, run
deepline csv show --csv <path> --summaryfirst to understand its shape (row count, columns, sample values) before deciding how to process it. - NEVER read a large CSV into context with the Read tool. Reading CSV rows into the conversation window exhausts context and produces zero output. This is the single most common failure mode.
- For row-by-row processing (enrichment, rewriting, research, scoring), use a Deepline play per §2.5: prebuilt if one fits, fork/wrap when close, author when not.
- To explore or understand CSV content without loading it, use
deepline csv show --csv <path> --rows 0:2for a two-row sample, or spawn an Explore subagent to answer questions about the data. - Pilot before scale: slice the CSV (
head -3 in.csv > pilot.csv), run the play on the slice,deepline runs exportand inspect, then run the full file. Small-input exception: when an exact-fit prebuilt covers a small input (≤ ~25 rows) whose scope the user already stated, the full file IS the pilot — run it once. - The pilot is never the deliverable. A task is not done until the FULL input has run and the result is exported to the exact requested output path:
deepline runs export <run-id> --out "$FINAL_CSV". Finishing with only a pilot CSV, or an export under a play-derived name, is the single most common way to fail the task while feeling done.
Tools
For signal-driven discovery (investor, funding, hiring, headcount, industry, geo, tech stack, compliance), start with deepline tools search. Do not guess fields. Its syntax is deepline tools search [query] [--categories <categories>] [--search_terms <terms>] [--json]: provide a query, or at least one of --categories and --search_terms. The query is optional only for structured filtering. Use commas for multiple categories or search terms. There is no --prefix flag; include a provider name in the query when needed.
Search 2-4 synonyms, execute in parallel only after the standalone preflight:
DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search investor
DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search "crustdata investor"
DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search --categories company_search --search_terms "structured filters,icp"
DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search --categories people_search --search_terms "title filters,linkedin"
2.5) Plays are the surface
For row-by-row processing (per customer, per lead, per LinkedIn URL), run a Deepline play via deepline plays run. deepline enrich is deprecated — do not use or document it; when no play fits, author one.
Discover live, then run.
deepline plays search <query>anddeepline plays describe <name>— choose from the live catalog and its contract, never from memory. Search results include arunCommandand acloneEditStarterfor every prebuilt.Prebuilt fits → run it. Batch prebuilts take a CSV directly:
deepline plays run prebuilt/name-and-domain-to-email-waterfall-batch --input '{"csv":"leads.csv"}', thendeepline runs export <run-id> --out "$FINAL_CSV".Close but not exact → pull and edit it. Every prebuilt is forkable:
deepline plays get prebuilt/<name> --source --out ./<name>.play.ts deepline plays check ./<name>.play.ts # mandatory before running deepline plays run --file ./<name>.play.ts --input '{...}'If
plays checkfails on a missing local import, that prebuilt is multi-file — wrap it instead of forking:deepline plays bootstrap <family> --from <source> --using play:prebuilt/<name> --limit 5 --out workflow.play.ts.No play fits → author one from scratch per recipes/deepline-plays.md: compose tools and other plays, map CSVs, add fallback logic and joins.
Results live in the Customer DB. Every batch play persists its dataset as a durable table: deepline db query --sql 'select * from "storage"."<table>" limit 20' --max-rows 20 --json (the run output names the table). Columns are the play's snake_case fields plus per-leg columns like email_result__hunter_email — the per-provider audit trail. Rerunning reuses filled cells instead of re-buying them; exports are projections of this table, so nothing is lost if a CSV goes missing.
The iterate loop — pilot, price, fix, then scale:
- Run a few rows (slice the CSV or run 2-3 scalar inputs).
- Read price and performance:
deepline runs get <run-id> --full --jsonreports billing and per-step outcomes; the storage table's per-leg columns show which providers hit, missed, or erred. - Fix what the pilot exposed BEFORE scaling: a provider that misses or flakes on your segment gets dropped or reordered in a fork; wrong columns get a
columnsmap; weak coverage gets a different route. Do not buy the same failure at full scale. - Run the full file, export to
FINAL_CSV, and report to the user: rows delivered, coverage, observed credits, and what you changed after the pilot.
3) Core policy defaults
3.1 Definitions and defaults
GTM time windows, thresholds, and interpretation rules are defined in the Definitions section of enriching-and-researching.md.
Provider Playbooks
Provider-specific playbooks are bundled as separate reference files. Open the relevant playbook when provider-specific behavior, pricing, caveats, or payload conventions matter.
adyntel, affinity, ai_ark, akta, allegrow, amplemarket, apify, attention, attio, aviato, bettercontact, bigquery, bloomberry, bluesky, bounceban, brave, browserbase, builtwith, clay, clickhouse, cloudflare, contactout, contextdev, crustdata, crustdata-v2, crustdata-v3, databricks, dataforseo, datagma, deepline_native, deeplineagent, discolike, dropleads, emailbison, emailguard, enformion, enigma, exa, findymail, firecrawl, fireflies, forager, fullenrich, generic_http, gong, google_ads_audiences, govfiles, grain, hackernews, harvestapi, heyreach, hubspot, hunter, icypeas, instantly, intercom, ipqs, kaspr, kernel, leadmagic, lemlist, limadata, linkedin_ads_audiences, luma, lusha, meta_audiences, nooks, openmart, opensosdata, openwebninja, outreach, parallel, peopledatalabs, pipedrive, podscan, postgres, predictleads, prospeo, quickenrich, rapidapi, redshift, salesforce, salesforge, salesloft, scrapecreators, searchbug, sec_edgar, sentrion, serper, smartlead, snowflake, socrata, sumble, theirstack, theswarm, trestle, twitterapi, upcell, versium, wiza, wizleads, zerobounce, zoho_crm, zoominfo
- Apply defaults when user input is absent.
- User-specified values always override defaults.
- In approval messages, list active defaults as assumptions.
3.2 Working directory — set up BEFORE any file writes
NEVER write files to /tmp/ or any absolute temp directory. Files in system /tmp/ are wiped on reboot — users permanently lose enriched CSVs, research outputs, and hours of paid enrichment work. This is a critical data-loss risk.
Set up a descriptive project-local working directory as your first action:
WORKDIR="deepline/data/<descriptive-task-slug>" && mkdir -p "$WORKDIR" && echo "$WORKDIR"
The slug must describe the task (e.g. deepline/data/yc-cmo-outbound, deepline/data/acme-email-waterfall). Do NOT use random names like mktemp generates — the user needs to find these files later. See enriching-and-researching.md for full details.
3.3 Output policy and User Interaction Pattern
- Always use a Deepline play for list enrichment or discovery at scale (>5 rows) — §2.5 routing. The run's play page lets the user inspect rows and rerun; send that URL.
- Even for company → ICP person flows, plays work: search and filter as part of the process, with providers like Apify to guide.
- Even when you don't have a CSV, create one and run the batch play against it.
- This process requires iteration; one-shotting via
deepline tools executeis short sighted. - In chat, send file/run links and render a decision table when rows inform a decision.
- Preserve lineage columns (especially
_metadata) end-to-end. When rebuilding intermediate CSVs with shell tools, carry forward_metadatacolumns. - Never overwrite a user-provided source CSV; write outputs to your working directory. Reruns of a play reuse completed cells by default.
See enriching-and-researching.md for deepline csv commands, pre-flight/post-run script templates, and inspection details.
3.4 Final file and run check (light)
- Keep one intended final CSV path:
FINAL_CSV="${OUTPUT_DIR:-$WORKDIR}/<requested_filename>.csv" - Before finishing: use the post-run inspection script pattern from enriching-and-researching.md. Run it once instead of separate checks.
- Checkpoint the deliverable. On multi-phase pipelines (companies → contacts → emails), write
FINAL_CSVas soon as the first complete rows exist and overwrite it as later phases improve it. A timeout or crash must leave the best-so-far file at the requested path — intermediates under other names do not count as delivery. - For a task that ran a Play, include its result. Give the exact
FINAL_CSVpath and play page link; do not invent one for monitor-only, research, or advisory work.
4) Credit and approval gate (paid actions)
This section's pilot, CSV preview, and full-run template governs enrichment,
sourcing, and other row-processing runs. Monitor mutations use the workflow in
recipes/deepline-monitors.md instead: inspect scope, reuse candidates,
downstream actions/unknown consumers, and price before asking. Keep that
approval decision-first: scope table, recommendation, live Deepline price, and
one question. Because a monitor can incur variable future event charges, require
explicit approval before every paid deploy, reactivate, or historical
widening—even when the user stated the scope. A historical rung is empty only
after its documented provider completion window; leave it intact while that
window is pending. Ask only when a material requirement is missing or the check
reveals an invalid or unaffordable configuration.
4.1 Required run order
- Pilot on a narrow scope: a small CSV slice through the same batch play, or 2-3 scalar runs.
- If the scope is NOT already approved (see below), request explicit approval.
- Run full scope, report the pilot's cost and quality findings alongside the deliverable.
User-stated scope = already approved for bounded row-processing only. When
the user's request itself states the full scope ("these 5 contacts", "~30
companies", "everyone in this CSV"), the request IS the approval: pilot to
validate quality and provider choice, then complete the stated scope, export to
FINAL_CSV, and deliver — reporting cost and per-provider performance with the
result, not as a blocking question. This exception never applies to monitors;
follow the monitor-specific consent gate above.
Stop and ask only when the scope is open-ended ("build me a big list"), the pilot reveals a problem worth a decision (low coverage, wrong matches, high cost per usable row), or projected spend exceeds a budget the user stated. Then present pilot results, projected cost, and the recommended route, and wait.
4.2 Execution sizing
- Use smaller sequential commands first.
- Keep limits low and windows bounded before scaling.
- For TAM sizing, a great hack is to keep limits at 1 and most providers will return # of total possible matches but you only get charged for 1.
- Prefer providers and plays that charge on returned results or successful hits when coverage is uncertain. If a provider bills per attempt/request/page, prove quality on a tiny pilot before letting it fan out.
- Stop after the pilot when the first rows show low usable coverage, wrong-person/company matches, missing getters, or high cost per usable row. Change route/provider order before buying the same failure at full scale.
- Do not depend on monthly caps as a hard risk control.
- Plan play spend before full scale by reading the play contract, using
deepline plays list --show-costfor its static or observed per-row/per-run estimate when available, then running a small pilot and inspecting actual spend withdeepline runs get <run-id> --full --json; label any extrapolation as an estimate. Onplays share publishandplays share update,--show-costinstead opts into average cost metrics on the public page when safe, comparable run samples are available.deepline plays runhas no cap flag, and the runtime-enforced--max-credits-per-run <credits>ceiling exists only on the deprecated legacy surface — never describe a play cap as enforced; the pilot plus stated estimate is the control.
4.2.1 Over-provision, then filter — never chase missing rows
When the user asks for N rows, start with ~1.4×N (e.g., 35 for 25). Every pipeline phase has natural falloff — contact search misses ~15-20% of companies, email waterfall misses ~5-10% of contacts. Fighting to complete the hard rows is almost always a waste: the companies that providers can't find contacts for are the same ones that won't have email coverage either.
Do this:
- Pull more candidates than needed at the top of funnel.
- Run the full pipeline (contacts → emails → outbound).
- At the end, filter to the best N complete rows and deliver those.
- Drop incomplete rows — don't retry or manually patch them.
Do NOT do this:
- Trim results to exactly N before running the pipeline.
- Spend turns retrying failed lookups with fallback providers,
deeplineagentresearch passes, or manual patching. - Run enrichment on all rows just to fill gaps in a few (especially broad
deeplineagentresearch passes).
Provider coverage is a property of the company, not something you can overcome with more effort. Tiny startups with 5 people will have zero coverage across all providers — no amount of retrying changes that. Over-provision at the top and let incomplete rows fall off naturally.
4.3 Approval message content
Include all of:
- Play or provider(s)
- Pilot summary and observed behavior
- Intent-level assumptions (3–5 one-line bullets)
- CSV preview from the real pilot: the head of
deepline runs export <pilot-run-id> --out - Credits estimate / range
- Full-run scope size
- Max spend cap (stated and monitored; no runtime-enforced play cap exists)
- Approval question:
Approve full run?
Strict format contract (blocking):
- Use the exact four section headers: Assumptions, CSV Preview (ASCII), Credits + Scope + Cap, Approval Question.
- If any required section is missing, remain in
AWAIT_APPROVALand do not run paid/cost-unknown actions. - Only transition to
FULL_RUNafter an explicit user confirmation to the approval question. run_javascriptis the non-AI path.ai_inferenceis for general classification/structured reasoning, anddeeplineagentis for context gathering / web research / signal extraction.
Approval template:
Assumptions
- <intent assumption 1>
- <intent assumption 2>
CSV Preview (ASCII)
<paste verbatim pilot output: the runs export head>
Credits + Scope + Cap
- Provider: <name>
- Estimated credits: <value or range>
- Full-run scope: <rows/items>
- Spend cap: <cap>
- Pilot summary: <one short paragraph>
Approval Question
Approve full run?
4.4 Mandatory checkpoint
- Must run a real pilot against the exact CSV intended for the full run: a small slice through the same batch play.
- Must include the pilot output preview verbatim in approval.
- If pilot fails, fix and re-run until successful before asking for approval.
- Ask for approval in chat after the pilot. Include the row count, estimated credits, and a small ASCII preview so the user can approve or redirect without opening another surface.
4.5 Billing commands
deepline billing balance # Show current credit balance
deepline billing usage # Show recent billing activity and grouped recent usage
deepline billing limit # Show the current monthly billing cap
When credits are zero or unavailable, stop paid work and ask whether the user
wants to add Deepline credits. If the balance or failure output includes a
recovery object, quote its top_up_command and checkout_command exactly,
including --json and --no-open; do not run them until the user approves.
Do not hardcode a USD-to-credit exchange rate in the skill. Use live billing,
pricing, or tool output when quoting credit costs.
5) Provider routing (high level)
Quick-reference summary only — the Section 2 sub-doc you already read is the authority.
- Search / discovery → You MUST have finding-companies-and-contacts.md open. It contains the parallel execution patterns, provider filter schemas, and provider mix tables. Start with
deepline tools search <intent>and execute field-matched provider calls in parallel; when thedeepline-list-buildersubagent is available, use subagent-based parallel search orchestration as the preferred pattern. Usedeeplineagentonly for synthesis or ambiguity resolution after the direct discovery path is exhausted. - Enrich / waterfall / coalesce → You MUST have enriching-and-researching.md open. It routes each scenario to a play and shows the
deepline plays runinvocation, plus waterfall patterns and coalescing logic. Do not restate play internals from memory; treat the play itself as the source of truth for exact provider order and gating. - Custom signals / messaging → Read enriching-and-researching.md (custom signals section). Use
run_javascriptfor deterministic transforms/template logic anddeeplineagentfor AI work. Start fromprompts.json. - Verification →
leadmagic_email_validationfirst, then enrich corroboration. - LinkedIn profiles, company employees, posts, comments, and reactions -> Prefer Deepline's native HarvestAPI provider. Use the documented
harvestapi_*names as starting hints, then rundeepline tools describe <operation> --schema-onlybefore execution; broad tool search can be noisy. Use Apify only when HarvestAPI does not expose the required LinkedIn surface. - For phone recovery, read enriching-and-researching.md and follow the notes/provider guidance there rather than relying on deleted numbered sections.
Before hand-rolling any pipeline a prebuilt might cover, deepline plays describe the candidate play and either use/wrap it or state the contract mismatch in one line. Silently bypassing a fitting prebuilt is a routing failure.
Provider path heuristics:
- Broad first pass: direct tool calls for high-volume discovery.
- Quality pass: AI-column orchestration with explicit retrieval instructions.
- For job-change recovery: prefer quality-first (
crustdata_v3_person_enrich,peopledatalabs_*) beforeleadmagic_*fallbacks. - Never treat one provider response as single-source truth for high-value outreach.
6) Additional notes
Critical: keep writing-outreach.md workflow context active when running any sequence task. It is not optional for ICP-driven messaging.
Operational troubleshooting: rate limits and CLI health
- Use Deepline plays for heavy row-by-row work whenever possible. The runtime has built-in rate-limit handling (adaptive retries/backoff) for standard upstream limits. If you are building a homegrown script, assume it does not include the same automatic protection unless you explicitly implement it.
- If enrichment or CLI behavior is unstable, update the CLI and reinstall the Deepline skills:
deepline update
deepline skills
Sites requiring auth: Don't use Apify. Tell the user to use Claude in Chrome or guide them through Inspect Element to get a curl command with headers (user is non-technical).
- If user provides actor ID/name/URL: use it directly.
- If not, search
deepline tools describe apify_run_actor_syncfor the actor id, or try deepline tools search. - If not present, run discovery search.
- Avoid rental-priced actors.
- For LinkedIn posts and engagers, use the native HarvestAPI operations first: inspect
harvestapi_search_posts,harvestapi_get_post,harvestapi_get_post_reactions, andharvestapi_get_post_comments. Usesupreme_coder/linkedin-postonly when the native provider does not cover the requested shape. Avoidsilentflow/linkedin-posts-scraper-pprandalizarin_refrigerator-owner/linkedin-post-scraperunless the user explicitly asks for them. - Pick high rating plus high usage/run count; when tied, choose best evidence-quality/price balance.
- Honor
operatorNotesover public ratings when conflicting.
deepline tools execute apify_list_store_actors --input '{"search":"similarweb traffic scraper","sortBy":"relevance","limit":20}'
deepline tools execute apify_get_actor_input_schema --input '{"actorId":"radeance/similarweb-scraper"}'
7) Feedback & session sharing
7.1 Proactive issue reporting (mandatory)
Without waiting for a request, run deepline feedback send once per issue
cluster when calls repeatedly fail, output is clearly wrong, a bug blocks work,
a workaround is substantial, a Play is disproportionately slow/stuck, or an
ordinary result takes avoidable product steps. Then continue the user's task.
Include the goal, tool/provider/model, failure and exact error, and attempted reproduction. For latency or excess steps, include the run ID, input size, duration/polling or actual path, and expected progress/direct path. Do not report legitimately long provider work.
For errors, pass --error-outcome terminal if work stopped or
--error-outcome continued if work continued. Omit it for non-errors.
deepline feedback send "Goal: <goal>. Tool: <details>. Failure: <error and repro>." --error-outcome <terminal|continued>
7.2 End-of-session consent gate (mandatory)
After the substantive result is resolved, ask exactly one Yes/No question in a
separate message—never beside a table, recommendation, approval request, or
unresolved user decision. Do not ask it after a terminal no-change outcome:
Change: none — I’m leaving it as is. must remain the final line of that
response.
Would you like me to send this session activity to the Deepline team so they can improve the experience? (Yes/No)
If user says:
- Yes -> run:
deepline sessions send --current-session --rating <good|bad|neutral> - No -> do not send the session.
Ask once per completed run; do not re-ask before a new run/session. Rate by outcome, not tone (see --help).
Files (gtm-eng-skills)
-
agents
-
execution-plan-creator.md 1.8 KB
--- name: execution-plan-creator description: Create a concrete Deepline execution plan before running GTM work. Use when the task needs routing, sequencing, provider selection, approval gating, or a plan that maps cleanly onto the skill docs. tools: Read, Grep, Glob, Bash model: haiku maxTurns: 8 --- You turn GTM requests into short, executable plans. Primary job: - Read the relevant GTM skill docs first. - Decide which phase doc or recipe governs the task. - Produce a concrete sequence of commands or workflow steps. - Call out where approval is required before any paid or cost-unknown full run. Mandatory workflow: 1. Read the matching phase doc: - Discovery, prospecting, company/contact search, portfolio sourcing: `finding-companies-and-contacts.md` - Enrichment, research, waterfall, column-level work: `enriching-and-researching.md` - Outreach, personalization, scoring, copy: `writing-outreach.md` 2. Check `recipes/` for an exact-match playbook before inventing a plan. 3. Build a minimal execution plan with clear stages, expected outputs, and provider choices. 4. Separate pilot steps from full-run steps. Planning rules: - Prefer direct URL fetch/extract over search when the data lives at a known public page. - Prefer `deepline plays run` (prebuilt or custom play) for row-level enrichment or repeated transforms. - For people search, avoid exact-title strategies; prefer broad function keywords plus seniority. - Do not guess provider schemas. If the plan depends on a provider, include a `deepline tools describe <tool_id>` validation step. - If the work is paid or cost-unknown, include the approval checkpoint explicitly. Output format: - Goal - Governing docs - Recommended approach - Step-by-step plan - Approval gate - Risks or assumptions Keep plans concise, operational, and ready for another agent or the parent agent to execute. -
list-builder.md 2 KB
--- name: list-builder description: Build company or contact seed lists for GTM workflows. Use for discovery, TAM building, portfolio prospecting, known-company contact finding, and provider-driven list construction before enrichment. tools: Read, Grep, Glob, Bash model: haiku maxTurns: 12 --- You build seed lists for GTM workflows using Deepline's documented search patterns. Primary job: - Read the discovery docs first. - Choose the right discovery path and provider mix. - Build a clean seed list or an execution-ready search plan. - Stop when the workflow transitions from discovery into per-row enrichment. Mandatory workflow: 1. Read `../SKILL.md`. 2. Read `../finding-companies-and-contacts.md`. 3. Read the matching recipe or play doc when applicable: - `build-tam.md` - `portfolio-prospecting.md` - `enriching-and-researching.md` for known-company contact finding / persona lookup 4. Decide which path applies: - Known URL or public directory: fetch/extract directly. - Structured ICP company search: shortlist the best provider, inspect schema, validate enums, then search. - Known companies, need contacts: use the documented contact-finding path. 5. If the task becomes row-level enrichment, hand off to `enriching-and-researching.md` instead of continuing with ad-hoc scripting. Execution rules: - Follow shortlist -> inspect -> validate -> execute. - Do not fire multiple providers blindly in parallel. - Do not guess payload fields or enum values. - Prefer broad role keywords plus seniority over exact job titles. - Filter and supplement; do not restart from scratch when some rows fail ICP checks. - Stop at good enough when coverage is sufficient. Deliverables: - For planning-only tasks: provide the chosen provider path, rationale, and the exact first commands to run. - For execution tasks: produce a seed CSV or a clearly structured list with source lineage. - Always note the handoff point when the next step should move into enrichment. Keep the output focused on useful rows, validated search choices, and minimal wasted credits.
-
-
provider-playbooks
-
adyntel.md 1000 B
Use Adyntel for paid-media intelligence (creative examples, channel presence, and ad strategy signals). - Deepline injects required Adyntel account identity fields automatically. - Normalize domains to bare host format (`company.com`, no protocol or `www`). - Use channel-native tools (`adyntel_facebook`, `adyntel_google`, `adyntel_linkedin`, `adyntel_tiktok_search`) before cross-channel synthesis. - Prefer `adyntel_google_shopping_sync` to launch and poll Google Shopping in one call. - Keep `adyntel_google_shopping_status` for manual follow-up polling when you already have an `id`. - Use `adyntel_tiktok_ad_details` only after collecting IDs from `adyntel_tiktok_search`; pass `id` as a string. - Billing is request-based for paid endpoints, so pre-filter targets before broad sweeps. ```bash deepline tools execute adyntel_google --payload '{"company_domain":"hubspot.com"}' ``` ```bash deepline tools execute adyntel_google_shopping_sync --payload '{"company_domain":"allbirds.com"}' ``` -
affinity.md 825 B
# Affinity API V2 Use these actions only with the workspace's connected Affinity API key. All actions target Affinity API V2 and pin version `2026-07-15`. Prefer reads before writes when resolving IDs. List, person, company, opportunity, note, and field IDs are provider identifiers, not display names. Actions whose names or descriptions say create, update, merge, send, or delete change the connected Affinity workspace. Confirm the intended resource and payload before executing them. Affinity applies the connected user's resource and endpoint permissions. Pagination, filters, beta status, and required permissions come from the dated official Affinity OpenAPI contract. Handle 429 responses with backoff because Affinity limits each authenticated user and also applies account-level monthly and concurrency limits. -
ai_ark.md 8.5 KB
# AI Ark Integration Guide ## Overview AI Ark provides company search, people search, reverse lookup, mobile phone finding, personality analysis, async export, and async email finding across enriched profiles. **Base URL:** `https://api.ai-ark.com/api/developer-portal` **Auth:** `X-TOKEN` header with API key. **Rate limit:** AI Ark documents a default limit of 5 requests per second per API token. Deepline queues calls at that rate within its provider scope; the queue hint does not coordinate separate organizations that use their own tokens. If AI Ark returns HTTP 429, wait for its `Retry-After` value before retrying. [AI Ark rate-limit guidance](https://docs.ai-ark.com/docs/ai-agents). ## Credit Costs | Operation | Cost | Unit | | ------------------------------ | ---- | --------------- | | Company Search | 0.1 | per result | | People Search | 0.5 | per result | | Reverse People Lookup | 0.5 | per request | | Mobile Phone Finder | 5.0 | per request | | Export People (with Email) | ~0.5 | per email found | | Personality Analysis | TBD | coming soon | | Email Finder | ~0.5 | per email found | | Polling / Statistics / Results | 0 | free | ## Filter Structure (CRITICAL) AI Ark uses a strict nested filter structure. **Do NOT flatten or simplify these structures.** Deepline still accepts legacy aliases for existing scripts, but new code should use the AI Ark-native fields below. ### Text filters Text filters require `{ mode, content }` inside `include`/`exclude`: ```json { "contact": { "fullName": { "any": { "include": { "mode": "SMART", "content": ["Ada Lovelace"] } } }, "experience": { "current": { "title": { "any": { "include": { "mode": "SMART", "content": ["VP", "Director", "Head of"] } } } } } } } ``` ### String filters String filters use arrays directly in `include`/`exclude`: ```json { "contact": { "seniority": { "any": { "include": ["vp", "director", "c_suite"] } }, "departmentAndFunction": { "any": { "include": ["sales", "marketing"] } } } } ``` ### Company filters Contact company filters use AI Ark company IDs, not company names. Get IDs from `ai_ark_company_search`, then use `latest`, `current`, or `previous`. ```json { "contact": { "company": { "previous": { "any": { "include": ["8b1c2fa5-3ceb-437e-5ef5-495bc6d34ace"] } } } } } ``` ### Large company exclusions For a customer-company blacklist, run Company Search and collect the returned `company_ref_id` values. Call `ai_ark_create_company_list` once with up to 10,000 `company_ref_ids` and a stable `idempotency_key`; it returns one `company_list_ref_id`, which can be supplied to Company Search as `company_exclusion_list_ref_ids` (up to 10 lists). The list expires after 24 hours. Deepline keeps the Ark list ID private: managed executions are isolated to the Deepline workspace, while an active customer Ark key uses that customer's native Ark account. Do not pass Ark company or list IDs to the list action. This is a **Company Search-only** feature. Ark's published People Search and Export People APIs accept `lists.people_id.exclude`, but not `lists.company_id.exclude`. Do not send `company_exclusion_list_ref_ids` to People Search or Export People; direct Ark API calls have the same endpoint limitation. ### Complete People Search example ```json { "page": 0, "size": 25, "account": { "domain": { "any": { "include": ["acme.com", "example.com"] } } }, "contact": { "experience": { "current": { "title": { "any": { "include": { "mode": "SMART", "content": ["VP of Sales", "Head of Sales"] } } } } }, "seniority": { "any": { "include": ["vp", "director"] } }, "location": { "any": { "include": ["San Francisco"] } } } } ``` ### Field type reference **Contact text filters** (use `{ mode: "SMART", content: [...] }`): `fullName`, `skill`, `certification`, `education.degree`, `education.fieldOfStudy`, `experience.latest.title`, `experience.current.title`, `experience.previous.title` **Contact keyword filter**: `keyword` uses `{ include: { content: [...] } }` with optional `sources`, not a normal text operand. **Contact string filters** (use `["value1", "value2"]`): `socialMediaLink`, `seniority`, `location`, `linkedin`, `departmentAndFunction`, `company.latest`, `company.current`, `company.previous`, `education.school` **Legacy aliases**: `name` maps to `fullName`; `title` maps to `experience.current.title`; `pastCompany` maps to `company.previous`; `currentCompany` maps to `company.current`; `contactLocation` maps to `location`; `socialProfile` maps to `socialMediaLink`. Do not send an alias and its native equivalent in the same request. **Account text filters**: `url`, `name`, `productAndServices`, `technologies` **Account string filters**: `domain`, `linkedin`, `socialMediaLink`, `phoneNumber`, `location`, `technology`, `naics` ### Common mistakes to avoid - Prefer `contact.experience.current.title` over legacy `contact.title` - Prefer `contact.company.previous` with AI Ark company IDs over legacy `contact.pastCompany` - ❌ `{ include: ["value"] }` for text filters → ✅ `{ include: { mode: "SMART", content: ["value"] } }` - ❌ `{ include: { mode: "SMART", content: ["value"] } }` for string filters → ✅ `{ include: ["value"] }` ## Recommended Workflow ### Prospecting 1. **Company Search** (`ai_ark_company_search`) to build account lists. Use `account` filters for firmographics, funding, technology, geography, and optional `lookalikeDomains`. 2. For a large existing-customer exclusion, make one `ai_ark_create_company_list` from the Company Search `company_ref_id` values, then use its `company_list_ref_id` in a second Company Search through `company_exclusion_list_ref_ids`. 3. **People Search** (`ai_ark_people_search`) uses its documented nested `account`, `contact`, and optional `lists.people_id` filters. It cannot accept the Company Search exclusion list; do not claim that an account blacklist carries into People Search. ### Email Finding (two paths) **Path A — Export People (recommended for bulk verified email pulls):** 1. `ai_ark_export_people` with filters + optional webhook → returns `trackId`. 2. Poll `ai_ark_export_statistics` until `state: "DONE"`. 3. Fetch results via `ai_ark_export_results` (paginated). **Path B — Find Emails from Search:** 1. Run `ai_ark_people_search` first → response includes a `trackId`. 2. `ai_ark_find_emails` with that `trackId` and a `webhook` URL (single-use, expires in 6 hours). 3. Poll `ai_ark_email_finder_statistics` until `state: "DONE"`. 4. Fetch results via `ai_ark_email_finder_results` (paginated). ### Identity Resolution - **Reverse Lookup** (`ai_ark_reverse_lookup`): Look up a person by email or phone number using the `search` field only. ### Phone Numbers - **Mobile Phone Finder** (`ai_ark_mobile_phone_finder`): Find mobile numbers by LinkedIn URL or name + domain. Use this after you already have a high-confidence person match because it is relatively expensive (5.0/request). ### Personality Analysis - **Personality Analysis** (`ai_ark_personality_analysis`): Analyze personality traits from a LinkedIn profile URL. ## Pagination All search/list endpoints use zero-based pagination with `page` and `size` parameters. Search and export creation use JSON body pagination; results browsing uses query params. ## Error Handling - **409 Conflict**: Export or email-finder result pages were requested while the async job is still processing. Poll statistics first. - **404 Not Found**: Profile not found (personality analysis). - **429 Too Many Requests**: Rate limit exceeded. Honor the response's `Retry-After` header when present; otherwise use a short exponential backoff. Do not assume a fixed 60-second reset window. ## Key Constraints - Export People: max 10,000 results per export. - Find Emails trackId: single-use, expires 6 hours after the People Search that generated it. - Webhooks: auto-retry up to 30 times. Use HTTPS endpoints and respond `200` immediately. - Undocumented endpoints in the vendor docs snapshot are intentionally not exposed here. -
akta.md 1.1 KB
# Akta agent guidance Start with `akta_company_search`. It accepts a company name, website, or Akta UUID and avoids guessing which identifier a later company action will resolve. Use `akta_industry_search` to translate a plain-language topic into Akta industry codes before constructing an industry-filtered news request. Treat the two search actions as resolvers: - inspect all returned matches; - prefer an exact website match over a name-only match; - keep the Akta UUID for subsequent lookups; - handle both an empty `data` array and a no-result status. If company search returns no match, do not work around the disabled Company Addition action. `akta_request_status` accepts only a request ID created through an approved Akta flow. Do not work around an unavailable action. Paid actions are intentionally gated until an internal account proves the runtime charge and response shape. For Product Reviews, do not convert the `products` array into a comma-separated string. The wire format repeats the query key. For Company Data and News filters, preserve comma-separated strings exactly as documented. -
allegrow.md 3.2 KB
# Allegrow Workflow Guidance Allegrow specialises in B2B catch-all domain resolution and primary-email identification. Use it as a final-layer validator after cheaper/faster verifiers (ZeroBounce, Findymail, etc.) have already run and returned `catch_all` or when you need to distinguish an executive's primary address from their secondary ones. ## Status Interpretation | `result.status` | Meaning | Action | |--------------------|------------------------------------------------------|--------------------| | `safe` | Valid, safe to send | **Send** | | `do_not_mail_abuse`| Valid address but high spam-report risk | Skip email; use LinkedIn/phone instead | | `some_risk` | Inconclusive, risk factors identified | Hold 30 days; re-validate before next send | | `block_bounce_risk`| Invalid; will hard-bounce | Remove from list | | `dead_email` | Invalid account on a catch-all host (won't bounce) | Use other channels; remove from active send list | | `spamtrap` | Spamtrap or trap-like mailbox | Remove from list | | `more_time_required`| Validation still processing | Poll later | | `missing_email` | Request did not include an email | Fix input | `result.subStatus`: - `primary`: Allegrow identified this as the contact's **main email**. Especially useful for execs and senior decision-makers who have multiple valid addresses. - `null`: No additional context. ## Usage Patterns - **After ZeroBounce returns `catch_all`**: Run Allegrow on those rows to resolve dead vs safe. Allegrow's B2B specialisation makes it materially better than generic verifiers on corporate domains. - **Executive lists**: Use `subStatus == primary` to prefer the primary address when multiple candidates exist. - **Do not use** `allegrow_validate` as a bulk first-pass verifier. It costs per call and is slower than SMTP-based verifiers. Reserve it for catch-all resolution and high-value contacts. - **Polling**: The sync endpoint has a 30-second timeout. A 202 response includes `pollUrl` and `retryAfter`. If your run context supports polling, retry after `retryAfter` seconds. In `deepline enrich` this is handled automatically. - **Async validation**: Use `allegrow_validate_async` only when a configured webhook or later polling via `allegrow_validate_status` fits the workflow. - **Bulk CSV**: Use the CSV job endpoints when the user explicitly needs Allegrow's bulk CSV workflow. `allegrow_create_csv_validation_job` only creates the job and upload URL; the CSV upload itself is a direct PUT to the returned presigned URL. ## Known Limitations - No balance/credit endpoint exposed in the API; quota resets are managed with your account manager. - owner.com and similar corporate domains return `dead_email` even for likely-valid addresses. Allegrow is conservative on fully catch-all domains with no MX activity evidence. Cross-reference against CRM data before removing these. - The exact purchased validation USD rate is not in the docs. Keep `ALLEGROW_PROVIDER_CURRENCY_TO_USD_RATE` blocked at 0 until the contract rate is supplied. -
amplemarket.md 4.5 KB
# Amplemarket Call `amplemarket_get_account_details` to verify a connection. Call capability-specific actions directly. Use people search or company search to find prospects. Use contacts and accounts to read and write CRM records. Use sequences and lead lists to start outbound work. Use tasks and calls to read workflow activity. Do not send REST work to the separate Amplemarket OAuth MCP server. All read actions and write actions use the Amplemarket workspace credential of the caller. Call a write action only when the user asks for that workspace change. ## Pagination Supply the page size in a `page` object. Do not supply an integer. ```json { "page": { "size": 25 } } ``` Amplemarket declares the page size as a string on 15 operations and as an integer on 2 operations. Deepline accepts both forms on every operation. Read the next-page cursor from `_links.next` in the response. Supply that cursor as `page.after`. ## Contact filters `amplemarket_get_contacts` needs a minimum of one filter. Supply `name`, `account_id`, or `ids`. Amplemarket rejects a request that has no filter. ## Contact creation `amplemarket_post_contacts` associates a contact with an account when you supply `company_domain` or `company_name`. The domain must match an active Prospect Hub account. Omit both fields when no account exists. Call `amplemarket_get_accounts` first to see the available accounts. An excluded domain does not prevent contact creation. Amplemarket applies the exclusion list to outbound sending only. Examine `amplemarket_get_excluded_domains` before you create contacts for a compliance-sensitive workflow. ## Phone numbers Amplemarket changes the format of a supplied phone number. The provider returned `+1 202-555-0123` for the supplied value `+12025550123`. Do not compare a returned number with a supplied number as a text string. A phone number that a caller supplies has the source `uploaded_by_user`. A phone number record also contains `kind` and `uploaded_id`. ## Actions that spend Amplemarket credits Deepline charges no credits for any Amplemarket action. The API is included with the customer's Amplemarket subscription and the key is customer-owned. These 5 actions spend the caller's own Amplemarket credit balance: - `amplemarket_post_people_enrichment_requests` - `amplemarket_get_people_find` - `amplemarket_post_email_validations` - `amplemarket_post_lead_lists` - `amplemarket_post_lead_lists_id_leads` Amplemarket documents person enrichment and person find as spending 0.5 or 1 email credit and 1 phone credit. Amplemarket does not publish which of its two email-credit amounts applies to a given call. Amplemarket documents email validation as spending 1 email credit per address. Lead-list creation and lead-list add inherit enrichment, validation, and reveal settings from the lead list, so they can spend credits; Amplemarket deducts that spend from the admin user of the account rather than the key owner. Tell the user which of their own credits a run will spend before you start a large batch. Amplemarket publishes no credit-balance endpoint, so neither Deepline nor an agent can read a remaining balance first: the live OpenAPI document has no balance, quota, or usage path, and `GET /account-info` returns only `id` and `name`. Amplemarket rejects a call with an `insufficient_credits` error once the balance runs out, so treat that error as an exhausted balance rather than a malformed request. Company enrichment polling, company find, sequence enrollment, and enrichment/validation result retrieval are not separately credit-consuming in the official documentation. Company data returned alongside a person enrichment arrives with that person call rather than as a separate charge. ## Disabled actions Deepline disables 4 actions. Each disabled action returns HTTP 403 with the code `INTEGRATION_PREREQUISITE_REQUIRED`. Deepline refuses the action before it calls the provider. A disabled action consumes no credits. Deepline disables these 4 actions because the official OpenAPI document has no 2xx JSON response example: - `amplemarket_get_calls_id_recording` - `amplemarket_post_phone_numbers_id_review` - `amplemarket_post_tasks_id_complete` - `amplemarket_post_tasks_id_skip` `amplemarket_post_sequences_id_leads` is enabled. Use that action to add leads to a sequence. ## Tasks The Amplemarket API has no action that creates a task. A sequence creates the tasks. A draft sequence creates no tasks. The API has no action that starts a sequence. Start a sequence in the Amplemarket user interface. -
apify.md 3.4 KB
Use Apify when you need controlled web automation/scraping workflows. - Use `apify_list_store_actors` first when you do not know the actor id yet. - **Results are ranked by quality score by default.** The top result is the most reliable actor based on rating, review count, total runs, and 30-day success rate. Pick the #1 result unless you have a specific reason not to. - Each actor in the response includes `_qualityScore` (higher is better), `_baseQualityScore`, and `_successRate30d` (percentage). Prefer actors with `_deeplineVetted: true`, high usage/rating, and `_successRate30d >= 95%`. - Do not route HarvestAPI-owned LinkedIn workflows through Apify. Prefer the native `harvestapi_*` provider operations for profiles, company employees, posts, comments, and reactions. For a LinkedIn shape the native provider does not expose, prefer `supreme_coder/linkedin-post` for generic post scraping. Avoid actors returned with `_deeplineDownranked: true` unless the user explicitly asked for that actor. - Build `actorId` as `username/name` from store results. - Use `apify_get_actor_input_schema` to inspect required/optional fields before running. - Wrapper-level fields (`actorId`, `input`, `params`, `timeoutMs`) and runtime validation behavior can differ from actor-page docs. - Prefer `apify_run_actor_sync` as the default execution path when you want results in one call. - Use `apify_run_actor` only when you need non-blocking execution, then poll run status before fetching outputs. - Validate payload shape with a tiny run before scaling row counts. - Deepline limits an organization to four unresolved Apify actor jobs by default. Additional calls receive a retryable Deepline capacity response before another billable actor run is launched; an explicit organization execution limit may override this guardrail. - A successful HTTP 200 with an empty dataset is reported as `meta.resultOutcome.classification = "ambiguous_empty"`, not proof of a genuine no-result. Apify currently does not identify whether the input was attempted. Do not automatically retry that result: the first run remains billable and a retry may create a second charge. ## Quality ranking Actors are ranked by: ``` score = rating * log2(reviews + 1) * log10(runs + 1) / 5 ``` Actors with less than 80% 30-day success rate are penalized. Actors with 0 reviews but high usage get a reduced fallback score. To bypass quality ranking and use Apify's native sort, pass `rankBy: "relevance"`. ## Examples ```bash # Search for actors, ranked by quality (default) deepline tools execute apify_list_store_actors --payload '{"search":"google play reviews","limit":5}' ``` ```bash # Search with Apify's native relevance sort deepline tools execute apify_list_store_actors --payload '{"search":"google play reviews","sortBy":"relevance","rankBy":"relevance","limit":5}' ``` ```bash # Inspect the actor's input schema page before execution deepline tools execute apify_get_actor_input_schema --payload '{"actorId":"neatrat/google-play-store-reviews-scraper"}' ``` ```bash # Run an actor synchronously deepline tools execute apify_run_actor_sync --payload '{"actorId":"neatrat/google-play-store-reviews-scraper","input":{"appIdOrUrl":"com.airbnb.android","sortBy":"newest","maxReviews":10},"timeoutMs":120000}' ``` ```bash deepline tools execute apify_get_dataset_items --payload '{"datasetId":"EU1bcB5F9gY3J1Zq2","limit":10,"offset":0}' ``` -
attention.md 449 B
# Attention Use Attention only with the workspace’s own API key. Start with read operations for conversations, teams, scorecards, and reports. Many published endpoints create, update, archive, delete, send, or trigger customer data and workflows. Review each generated operation description and required identifiers before executing a write. Attention API keys are sent in the `Authorization` header. Deepline does not bill for Attention usage. -
attio.md 5 KB
# Attio CRM — Agent Guidance ## Quick Reference | Goal | Operation | Notes | | ------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | Upsert person by email | `attio_assert_record` (object: `people`, matching_attribute: `email_addresses`) | Preferred over `create_record` — no conflict errors. | | Upsert company by domain | `attio_assert_record` (object: `companies`, matching_attribute: `domains`) | Attio auto-enriches when domain is provided. | | Search by name/keyword | `attio_search_records` | Fuzzy, max 25 results. Not real-time. | | Filter records precisely | `attio_query_records` | Structured filters, pagination, sorting. Use this for production queries. | | Add to pipeline | `attio_create_entry` or `attio_assert_entry` | Use assert for upsert behavior. | | Query pipeline entries | `attio_query_entries` | Supports same filter syntax as record queries. | | Log activity | `attio_create_note` | Supports markdown format. | | Assign follow-up | `attio_create_task` | Link to records, set deadline, assign workspace member. | | Discover schema | `attio_list_attributes` | Always check available attributes before writing unfamiliar values. | | Verify API key | `attio_identify` | Free. Returns workspace info and scopes. | ## Playbooks ### Playbook 1: Enrichment Roundtrip The most common workflow. Attio auto-enriches records when email (people) or domain (companies) is provided. ``` 1. attio_assert_record -> upsert person/company by email/domain 2. Attio auto-enriches -> job titles, social profiles, company data 3. attio_query_records -> poll or re-query updated enrichment fields before downstream sync ``` ### Playbook 2: Pipeline Qualification ``` 1. attio_query_records -> filter by enrichment criteria (employee_count > 50, funding stage) 2. attio_create_entry -> add qualified records to "Sales Pipeline" list 3. attio_create_task -> assign follow-up to rep (round-robin) 4. attio_create_note -> log qualification reasoning ``` ### Playbook 3: Meeting Follow-Up ``` 1. attio_assert_record -> upsert contact by email 2. attio_assert_record -> upsert company by domain 3. attio_create_note -> meeting summary (markdown format) 4. attio_create_task -> action items with deadline 5. attio_assert_entry -> create/update deal in pipeline ``` ### Playbook 4: Batch Import ``` 1. Loop: attio_assert_record -> upsert each record (respect 25 writes/sec) 2. Loop: attio_create_entry -> add to pipeline (respect 25 writes/sec) 3. attio_query_records -> verify enrichment state before routing follow-up tasks ``` ## Common Mistakes - **Use `assert_record` (PUT) not `create_record` (POST) for upserts.** POST fails with 409 on unique attribute conflicts. Assert always succeeds — creates if missing, updates if found. - **Multiselect append behavior on assert:** When the matching attribute is multiselect, new values are appended (existing preserved). Non-matching multiselect attributes are fully replaced. Plan accordingly. - **`search_records` is fuzzy and eventual.** It caps at 25 results and is not real-time. Use `query_records` with structured filters for precise, paginated queries. - **No bulk API.** Each record is created or updated individually. Stay within rate limits. - **Note event gotcha:** `note.updated` only fires for title changes. Use `note-content.updated` to track body edits. - **Enrichment is automatic.** Attio enriches records when email (people) or domain (companies) is provided. There is no explicit enrichment API call. - **Always check schema first.** Before writing to an unfamiliar object, call `attio_list_attributes` to discover available attribute slugs and types. ## Rate Limits | Type | Limit | | ---------------- | --------------------- | | Read requests | 100/sec | | Write requests | 25/sec | | Webhook delivery | 25/sec per target URL | All actions handle `429` responses by respecting the `Retry-After` header. For batch imports, pace writes to stay under the 25/sec write limit. -
aviato.md 797 B
# Aviato Use `aviato_get_balance` first when validating credentials or checking account state. Use preview enrichment (`preview: true`) when you only need free identity resolution fields. Non-preview person/company enrichment and bulk enrichment can consume Aviato credits. For `aviato_company_search` and `aviato_person_search`, use Aviato DSL shape: `{ "dsl": { "offset": 0, "limit": 5, "filters": [{ "name": { "operation": "textcontains", "value": "Polychain" } }] } }`. Do not use `{ "field": "name", "operation": "contains", "value": "Polychain" }`; Aviato rejects that filter object. Do not use disabled Aviato operations until Aviato supplies the complete endpoint credit ladder and the purchased-credit USD rate. Disabled operations are present only to keep OpenAPI coverage explicit. -
bettercontact.md 1.7 KB
# BetterContact Agent Guidance `bettercontact_get_result` returns each contact's `custom_fields` as an array of `{name, value, position}` records. Preserve that list shape; do not treat it as an object keyed by custom field name. ## Key patterns - **Enrichment is upstream-async.** By default, `bettercontact_enrich` and `bettercontact_bulk_enrich` wait briefly for terminal results. If the job is still running, they return a pollable request id; set `wait_for_completion: false` for launch-only behavior. Use `bettercontact_get_result` to fetch terminal results. - **Email status hierarchy:** deliverable > catch_all_safe > catch_all_not_safe > undeliverable. Only trust deliverable and catch_all_safe for outreach. - **Batch up to 100 contacts** per enrichment request using `bettercontact_bulk_enrich`. - Use the launcher response `id` as the `request_id` for `bettercontact_get_result`. - **Rate limit:** 600 requests per minute per API key, shared across all endpoints. This is BetterContact's confirmed going-forward limit; its public rate-limit page still shows the previous 60 RPM value. ## Pricing - Deepline bills from the terminal enrichment result, and charges only for successful lookups. - **Phone enrichment costs significantly more than email** — only enable `enrich_phone_number: true` when explicitly needed ## When to use - Use BetterContact when you need waterfall email enrichment with multi-provider verification. - Good fallback when single-provider finders (LeadMagic, Prospeo) miss. - Includes triple email verification, phone verification, contact & company enrichment. ## When NOT to use - Don't use for email validation only — use a dedicated validator. - Don't use for company/org enrichment — BetterContact is contact-focused. -
bigquery.md 1019 B
Use BigQuery when the task requires querying an organization's warehouse data. BigQuery runs over Google's HTTPS API. Prefer OAuth for user-delegated access and service accounts for workload access. Never ask users to base64-encode a key or write it to a temporary file. Use `maximumBytesBilled` when the user supplies a scan budget. Prefer `bigquery_run_semantic_query` when a saved semantic layer exists or the user asks for business metrics, dimensions, filters, funnels, or model-defined entities. The semantic query tool renders the stored BigQuery semantic layer into SQL and returns both rows and the rendered SQL for inspection. Use `bigquery_run_query` only when the user provides raw SQL, asks for direct SQL, or the semantic layer does not cover the requested analysis. Leave `write` unset for read-only `SELECT` or `WITH` SQL. Set `write: true` only when the user intentionally wants a single statement that may modify or overwrite customer data. Never expose BigQuery warehouse spend as Deepline spend. -
bloomberry.md 935 B
# Bloomberry Agent Guidance Use Bloomberry when the user needs account-level B2B technographic signals or hiring signals. Prefer: - `bloomberry_get_company_tech_stack` when you already know a company domain and need the vendors it currently uses. - `bloomberry_get_tech_stack_changes` when the user needs recent adoption, churn, or usage signals. Provide exactly one of `category` or `vendor_name`. - `bloomberry_get_current_customers` when building a list of companies currently using a vendor or category. Provide exactly one of `category` or `vendor_name`. - `bloomberry_list_vendors` before vendor-filtered searches when the exact vendor/category name is uncertain. - `bloomberry_search_job_postings` for hiring intent. Provide at least one of `keyword`, `normalized_job_titles`, or `domain`. Use `limit` conservatively on result-list endpoints because Bloomberry charges credits per returned signal, customer, or job posting. -
bluesky.md 139 B
# Bluesky Guidance Use `bluesky_search_posts` for public Bluesky posts. Use `sort: "latest"` and `since` for last-30-days style research. -
bounceban.md 1.6 KB
# BounceBan Guidance Use BounceBan to verify email deliverability, particularly when an address may be accept-all or protected by a secure email gateway. - Use `bounceban_verify_single` for a single email. It may return `status: verifying`; poll `bounceban_get_single_status` with the returned id instead of submitting it again. - Use `bounceban_verify_bulk` to create a batch from known email addresses. A Play waits for the terminal result while it is running; its run log prints the BounceBan task ID and a ready-to-copy status command as soon as the task starts. Status checks back off to 30 seconds to reduce repeated polls; if the provider returns 429, Deepline waits for `Retry-After` before the next check. - If a run is canceled or times out, the remote BounceBan task may still be active. Use `deepline tools execute bounceban_get_bulk_status --input '{"id":"<task-id>"}' --json` to check it, then `bounceban_get_bulk_emails` after it finishes. Status and result reads are free; the accepted verification task may still incur Deepline credits under its normal pricing. - The account, status, and result retrieval actions are free. Do not submit an address again while its verification is still running. - Completed raw responses may wrap the verdict at `toolResponse.raw.data.data.result` and its score at `toolResponse.raw.data.data.score`. Use `email_status` for the normalized Deepline verdict; do not treat a response envelope's outer `result` key as the email verdict. - The waterfall endpoint is not available yet: its documented HTTP 408 can retain a billable task, which needs a non-2xx async settlement path in the shared V2 runtime. -
brave.md 354 B
- Use `brave_web_search` for privacy-focused public web searches. - A call is billed once even if Brave returns no matching results; a provider 404 is not charged when its response contains no billing evidence. - Provider credentials, marketplace hostnames, and upstream billing counts are implementation details and are not part of the customer result. -
browserbase.md 1 KB
# Browserbase Agent Guidance Use Browserbase Search when you need web results without opening a browser. Use Browserbase Fetch when you need page content and do not need interaction. Use sessions when the caller will connect Playwright, Puppeteer, or another CDP client to a managed browser. Public session creation disables Browserbase logs and recordings and does not accept contexts, extensions, certificate IDs, external proxy credentials, project IDs, or arbitrary metadata. Do not plan workflows around updating a context. Browserbase deprecated API context uploads and has removed the update endpoint from its API, so contexts can only be created, read, and deleted. Do not use Browserbase Agents for customer workflows. Agent runs accept shared-project resources and sensitive free-form data without a tenant-scoped provider boundary or a zero-retention control, so Deepline disables this tool. This connector uses Deepline-managed Browserbase credentials. Customers are charged Deepline credits. Never expose Browserbase provider spend. -
builtwith.md 1 KB
# BuiltWith Guidance - Use `builtwith_domain_lookup` when you already know the domain and need live/current technographics. The handler defaults `live_only` to true; set `live_only=false` only when historical detections matter. - Use `builtwith_vector_search` to discover the exact BuiltWith technology label before `builtwith_lists` or `builtwith_trends`. Free-text tech guesses often miss if the BuiltWith canonical name differs. - Use `builtwith_bulk_domain_lookup` for row-heavy domain work. It auto-polls queued jobs by default and normalizes both sync and async paths to the same `results[]` shape. - The batch compiler only coalesces `builtwith_domain_lookup` calls when the request can be losslessly represented by the native bulk API. Requests using `trust`, `no_attr`, or date-range filters stay single-call. - `builtwith_lists` is best for account sourcing by technology; `builtwith_relationships`, `builtwith_redirects`, and `builtwith_tag_lookup` are better for niche infrastructure signals and related-domain expansion. -
clay.md 875 B
# Clay Use Clay only with the workspace's own API key. Start with `clay_get_public_api_me` to confirm the user and workspace. For structured search, call `clay_fields` before `clay_create_filters`, then page with `clay_run`. For advanced query mode, read `clay_query_mode_reference`, create the search with `clay_create_query_mode`, then page with `clay_run_query_mode`. Routine runs are asynchronous. Call `clay_run_routine`, then poll `clay_get_run_results` with the returned `routine_run_id`. For large JSONL inputs, first request an upload URL, upload the file directly to that URL, start the batch with `clay_start_routine_run_batch`, and poll `clay_get_routine_run_batch_results`. `clay_query` reads existing Clay table data and is available only on eligible enterprise Clay plans. Deepline does not bill for Clay usage; Clay account limits and charges still apply. -
clickhouse.md 1.1 KB
# ClickHouse Cloud Use this provider only with the workspace's own ClickHouse Cloud API Key ID and key secret. Configure both in [Dashboard > Integrations](https://code.deepline.com/dashboard/integrations), not as Play secrets. An org admin can also call `POST /api/v2/integrations/connect` with `provider: "clickhouse"` and `credentials: { "api_key": "<key-id>", "api_secret": "<key-secret>" }`. Start with list or get operations to discover organization and service IDs. To access warehouse data, first create a saved Query Endpoint in ClickHouse, then call `clickhouse_query_endpoint_run_get` for simple scalar variables or `clickhouse_query_endpoint_run_post` for complex variables. These actions execute saved SQL, not arbitrary SQL. The selected database role controls the endpoint's data access. Treat writes with care. Service, backup, API-key, member, ClickPipe, ClickStack, and Postgres operations can change or delete customer resources. A saved Query Endpoint can also write data when its configured SQL and database role allow it. Use a least-privilege API key: ClickHouse developer keys are read-only for assigned services, while admin keys can make changes. -
cloudflare.md 2.3 KB
# Cloudflare Browser Rendering — Agent Guidance ## When to use - You need to crawl a website and extract structured content (markdown, HTML, or JSON). - You need browser-rendered pages (JavaScript-heavy SPAs, dynamic content). - You want a managed crawl that follows links up to a configurable depth/limit. ## Key parameters - **`url`** (required): The starting URL. - **`limit`**: Max pages to crawl. Default 10. Keep low unless the user needs broad coverage. - **`depth`**: Max link depth from the starting URL. Default 100,000. - **`source`**: URL discovery method — `"all"` (default), `"sitemaps"`, or `"links"`. - **`formats`**: Array of `"html"`, `"markdown"`, `"json"`. Default `["markdown"]`. Markdown is best for LLM consumption. JSON uses Workers AI for extraction and requires `jsonOptions`. - **`render`**: Browser rendering toggle. Default `true`. Set `false` for simple static pages to save browser seconds. - **`options.includePatterns`** / **`excludePatterns`**: Wildcard patterns to filter which URLs get crawled. Exclude takes priority. - **`timeoutMs`**: How long to poll before returning partial results. Default 5 minutes. ## Timeout behavior - On timeout, the action returns whatever partial results are available with `timedOut: true`. - The `jobId` is included so you can construct a follow-up poll if needed. - The action does NOT throw on timeout. ## Job statuses - `running` — crawl in progress (non-terminal) - `completed` — all pages crawled successfully - `errored` — unrecoverable error - `cancelled_due_to_timeout` — exceeded 7-day max runtime - `cancelled_due_to_limits` — hit account browser time limits - `cancelled_by_user` — manually cancelled ## Cost awareness - `render: true` crawls are billed on browser time, post-deduct, based on `browserSecondsUsed` in the response. - `render: false` crawls use 0 browser-seconds (free during beta). - Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. - Keep `limit` reasonable — large crawls consume significant browser time. - Crawl jobs expire after 7 days; results available 14 days post-completion. ## Robots.txt - Cloudflare respects `robots.txt` by default, including `crawl-delay`. - Blocked URLs appear in results with `"status": "disallowed"`. -
contactout.md 7 KB
# ContactOut — Agent Guidance ## When to use ContactOut for LinkedIn → email/phone enrichment when you have a LinkedIn URL. High accuracy for active LinkedIn users. Strong for US + global. Falls after dropleads in cost-ordered waterfalls. **Important**: Free pre-check APIs (`contactout_check_email_status`, `contactout_check_work_email`, `contactout_check_personal_email`, `contactout_check_phone`) are informational only. Do not use an empty pre-check to skip a paid reveal: it can be empty even when ContactOut's paid reveal returns verified contact data. ## Provider characteristics - **Input required**: LinkedIn URL (best), email, or name+company - **Geographic coverage**: Global, strongest in US + Europe - **LinkedIn URL requirement**: Must contain "linkedin.com/in/" or "linkedin.com/pub/". Sales Navigator URLs not supported. ## Key operations ### contactout_linkedin_contact_info Uses ContactOut's Contact Info API (`GET /v1/people/linkedin`) for one LinkedIn profile. For personal-email waterfalls, call this instead of `contactout_enrich_person`: ```json { "profile": "https://www.linkedin.com/in/johndoe", "email_type": "personal" } ``` `email_type` accepts `personal`, `work`, `personal,work`, or `none`. ContactOut only consumes email credits when emails are returned; `email_type: "none"` returns no emails and consumes no email credits. `include_phone: true` can consume phone credits when phone numbers are returned. A customer credential connected in the dashboard always overrides the managed personal and work credential lanes. Managed-key routing follows ContactOut's endpoint entitlements: `GET /v1/people/linkedin` and the work-email status checker use the work key; `POST /v1/people/identifiers` uses the managed-only hashed key; enrich, search, domain, and personal-status endpoints use the personal/default key. The connector does not currently expose either LinkedIn batch endpoint. ContactOut does not document a per-call charge response header for this endpoint. Deepline billing is therefore locked to the documented response fields: one email credit when any returned email bucket is non-empty, plus one phone credit when a phone bucket is non-empty. ### contactout_get_hashed_email_identifiers Converts a batch of 5–100 LinkedIn profile URLs into hashed email identifiers for privacy-safe paid-ads audience matching. Use it to raise Meta/Google match rates without handling raw personal emails. ContactOut returns a flat `matches.emails` hash list plus a request-scoped `matches_found` count. One matched profile can return several hashes, so the hash count is NOT the matched-profile count. Deepline bills per `matches_found`, never per returned hash. A batch where nothing matches returns HTTP 404 `No hashed emails found`, which Deepline maps to an unbilled empty result. Requests with fewer than 5 unique profiles are rejected by ContactOut with HTTP 400. Because the hash list is unattributed, you cannot map a specific hash back to a specific input profile. Treat the output as an audience-level hash pool, not as per-row enrichment. Managed requests use the dedicated hashed API credential. ### contactout_check_email_status (FREE convenience helper) Checks work-email and personal-email availability together for one LinkedIn profile. ```json { "profile": "https://www.linkedin.com/in/johndoe" } ``` Returns: - `contactout_check_email_status` → `{ "has_personal_email": false, "has_work_email": true, "status": "verified" }` ### contactout_check_work_email / contactout_check_personal_email / contactout_check_phone (FREE — informational only) Check whether a LinkedIn profile has work email, personal email, or phone coverage. Zero credits consumed. Their results are not authoritative and must not gate `contactout_linkedin_contact_info` or `contactout_enrich_person`. ```json { "profile": "https://www.linkedin.com/in/johndoe" } ``` Returns one channel-specific payload per tool: - `contactout_check_work_email` → `{ "has_work_email": true, "status": "verified" }` - `contactout_check_personal_email` → `{ "has_personal_email": false }` - `contactout_check_phone` → `{ "has_phone": true }` ### contactout_enrich_person Enriches a person by LinkedIn URL (preferred), email, or name+company. Returns email array at `email`, `work_email`, `personal_email`. ```json { "linkedin_url": "https://www.linkedin.com/in/johndoe", "include": ["work_email"] } ``` ```json { "first_name": "John", "last_name": "Doe", "company_domain": "acme.com" } ``` ### contactout_count_people Free audience sizing. Takes the same filters as `contactout_search_people` and returns only `total_results`. Consumes no credits, so run it before any paid search. ```json { "job_title": "(VP OR Head) Sales", "company_size": "201-500", "location": "United States" } ``` ### contactout_search_people Search people by title, company, location, seniority. Set `reveal_info: true` to also retrieve emails (costs search + email credits). **This is never a free call.** Search bills 1 credit per returned profile, and `reveal_info: false` does not change that. It only gates the extra email/phone credits, so a `reveal_info: false` search is a full-price search, not a count or discovery mode. Use `contactout_count_people` when you want a count. ContactOut controls the page size and exposes no page-size parameter, so you cannot ask for fewer results. One call bills for every profile on the page even if you only need a handful. Narrow the filters to change *who* comes back; you cannot change *how many*. The number actually billed comes back as `metadata.page_size`. ```json { "job_title": "(VP OR Head) Sales", "company_size": "201-500", "location": "United States", "reveal_info": false } ``` Boolean logic supported: `"(Sales AND CRM) NOT Manager"` ### contactout_enrich_domain Enriches company data (size, industry, funding, HQ) from a domain name. ```json { "domain": "salesforce.com" } ``` ## Output shape `contactout_enrich_person` returns a flat profile object. Email at `email[0]`, `work_email[0]`, or `personal_email[0]`. No nested envelope. `contactout_linkedin_contact_info` returns the same flat profile object, but profile-only responses with no email or phone data are treated as no-result for billing and waterfall control. `contactout_search_people` returns `{ profiles: [...], metadata: { total_results: N } }`. ## Anti-patterns - Don't use Sales Navigator or Recruiter URLs — they'll return 400 - Don't use an empty free checker result to suppress a paid reveal — only the paid reveal determines whether ContactOut returns usable contact data - Don't include "http://" or "www." in domain values for `enrich_domain` - Don't treat `reveal_info: false` as a free or count mode — it still bills 1 search credit per returned profile. Size the audience with `contactout_count_people`, which is free - Don't call `contactout_search_people` when you only need a handful of profiles and the audience is unsized — there is no page-size parameter, so the call bills for the whole page regardless of how many you wanted -
contextdev.md 582 B
# Context.dev Use scrape or retrieval actions for one-off reads. Use crawl, extract, batch, monitor, and WebDB actions only when the broader or persistent workflow is intentional. Read the current resource before updating or deleting it. Confirm IDs before monitor runs, batch cancellation, WebDB sync or reprocessing, purges, and deletes. These operations can change customer-owned state. Start with `contextdev_get_web_scrape_markdown` for a single page and `contextdev_post_web_crawl` for a bounded site crawl. Keep page limits and timeouts small while validating a workflow. -
crustdata-v2.md 680 B
# CrustData V2 Guidance Use autocomplete before structured CompanyDB or PersonDB searches when a field expects canonical values. Prefer in-DB search for broad list building and reserve realtime search/enrichment for freshness-sensitive cases. For job data, use `crustdata_v2_job_search` for indexed job-listing discovery and `crustdata_v2_live_job_search` when the user explicitly needs current professional-network jobs for one known CrustData company id. Legacy dataset-table compatibility actions are hidden from discovery and should not be selected for new workflows. Do not expose provider credit ladders to customers; Deepline tool pricing should remain Deepline-facing. -
crustdata-v3.md 5.3 KB
# CrustData V3 guidance Use autocomplete before search when filter values are uncertain. Autocomplete is free and reduces expensive zero-result searches. Use indexed search for discovery: - `crustdata_v3_person_search` - `crustdata_v3_company_search` - `crustdata_v3_job_search` Keep `limit` strict. Search is billed per returned result, not by matched `total_count`. Filter to people with verified work emails before paying for enrichment. Person search accepts `experience.employment_details.current.business_email_verified` as a filter field (`type: "="`, `value: true`) — it selects only people whose current employment carries a verified business email, so every returned result is enrichable to a valid work email. The flag is a boolean, not the address: the emails themselves come back from person enrich under `contact.business_emails[*].email`, each with a `status` field (`verified` / `unverified`). Never read the profile-level email field or `contact.personal_emails` when the goal is work emails. ## Filter syntax and operators Person/company/job search all take a `filters` condition group: `{"op": "and", "conditions": [{"field": "...", "type": "=", "value": ...}]}` - groups nest, and `op` accepts `and` / `or`. Condition `type` supports `=`, `!=`, `in`, `not_in`, `contains`, `has_all`, `all_of`, `>`, `<`, `=>`, `=<`. Sorting is `sorts: [{"field": "...", "order": "asc" | "desc"}]`. Company and job search have their own filter/sort field vocabularies - read the field lists in each tool's input schema (`deepline tools describe`) rather than reusing person fields. ## Company-search response projections For `crustdata_v3_company_search`, `filters` and `fields` are different vocabularies. A field path that can filter companies is not necessarily a response selector. Request response groups such as `basic_info`, `headcount`, `funding`, `locations`, and `taxonomy`, then read nested values from the returned group. Do not request `basic_info.industries` or period leaves such as `headcount.growth_percent.6m`; use `basic_info` and `headcount.growth_percent` respectively. `roles`, `skills`, `seo`, and `competitors` are filter-only for this endpoint. ## Size and qualify for free before paying - `limit: 1` returns `total_count` and `total_count_relation` - TAM sizing for the price of one result. - `preview: true` (person search and person enrich) returns basic fields at preview billing - confirm identity/shape before buying full records. - Person search results include `contact.has_business_email`, `has_personal_email`, and `has_phone_number` booleans - you can see contact availability per person before spending on enrichment. - `fields: [...]` on search and enrich limits the returned field paths; on enrich, requested field groups drive the price - request only what the workflow uses. ## High-leverage person filters most workflows miss - **Job changes**: `recently_changed_jobs` filters to people who recently switched roles; pair with `metadata.updated_at` (range operators) to bound data freshness. - **Alumni prospecting**: the `experience.employment_details.past.*` family (`past.company_name`, `past.company_id`, `past.company_linkedin_profile_url`, `past.company_headcount_range`, `past.company_industries`) finds everyone who USED to work somewhere - "ex-Stripe, now at a 11-200 person company" is two conditions. - **Open to work**: `professional_network.open_to_cards` (values like `CAREER_INTEREST`, `HIRING_MANAGER`) surfaces people signalling openness. - **Normalized titles**: prefer `basic_profile.normalized_title.matched_title`, `.department`, and `.sub_department` over raw title string matching - it is CrustData's normalized taxonomy and beats regex title lists. - **Employer size without a company join**: `experience.employment_details.current.company_headcount_range` / `company_headcount_latest` filter people by their employer's size directly. - **Seniority and function**: `experience.employment_details.current.seniority_level` and `.function_category` are the org-chart building blocks. - **Influence and tenure**: `professional_network.connections`, `.followers`, and `years_of_experience` support scoring and champion selection. Use enrich after narrowing candidates: - `crustdata_v3_person_enrich` for full cached person profiles. - `crustdata_v3_person_contact_enrich` for contact-only lookups. - `crustdata_v3_company_enrich` for full company records. Use `crustdata_v3_company_identify` before company enrich when the inbound identifier is fuzzy. It is free. Prefer a domain or LinkedIn company URL. Name-only matching can return unrelated companies even at `confidence_score: 1.0`; treat those results as candidates and verify an independent identifier before changing stored names or domains. Some `crustdata_v3_person_enrich` field groups (for example `certifications`, per CrustData's own docs) may 403 with a permission error depending on the account's CrustData entitlement — this is not restricted in the schema because a different account may have different access. If a caller hits `PROVIDER_AUTHORIZATION_FAILED` requesting a specific field group, drop it and retry without that group rather than assuming every documented group is universally available to every account. Do not use old PersonDB field paths with V3 unless a reviewed compatibility mapper converts them to the documented `2025-11-01` field vocabulary. -
crustdata.md 4 KB
Use CrustData for structured discovery and enrichment with recall-first filtering. - Start with free autocomplete (`companydb_autocomplete`, `persondb_autocomplete`) to discover canonical values. - Default to fuzzy contains operator `(.)`; use strict `[.]` only when explicitly requested. - Use `crustdata_companydb_search` as the resolver step; for `crustdata_enrich_company`, prefer domain-based inputs instead of name-only payloads. - For job data, use `crustdata_v2_job_search` for indexed listings or `crustdata_v2_live_job_search` when freshness is required for a known CrustData company id. Legacy job-listing compatibility actions are hidden from discovery and should not be selected for new workflows. - In changed-company email recovery, use Crust as the second step after LeadMagic and before PDL. - Keep filters composable and inspect a small sample before adding expensive enrichments. - If Crust misses in the first 10 rows for a batch, move it later for the rest of that batch. ### Filter format `filters` accepts an array of condition objects (AND-combined automatically). Each condition: `{"filter_type":"<field>","type":"<operator>","value":"<val>"}` or `{"filter_name":"<field>","type":"<operator>","value":"<val>"}`. `filter_name` is syntactic sugar for `filter_type`; human-friendly aliases (e.g. `company_investors` → `crunchbase_investors`, `company_funding_stage` → `last_funding_round_type`) are auto-mapped. A single condition object (not in array) also works. **Company filter_type values:** `company_name`, `company_website_domain`, `linkedin_industries`, `hq_country`, `hq_location`, `region`, `year_founded`, `employee_metrics.latest_count`, `employee_count_range`, `employee_metrics.growth_6m_percent`, `employee_metrics.growth_12m_percent`, `employee_metrics.growth_12m`, `follower_metrics.latest_count`, `follower_metrics.growth_6m_percent`, `crunchbase_investors`, `tracxn_investors`, `crunchbase_categories`, `crunchbase_total_investment_usd`, `last_funding_date`, `last_funding_round_type`, `estimated_revenue_lower_bound_usd`, `estimated_revenue_higher_bound_usd`, `linkedin_id`, `linkedin_profile_url`, `company_type`, `acquisition_status`, `ipo_date`, `largest_headcount_country`, `markets`, `competitor_ids`, `competitor_websites`. **Person filter_type values:** `current_employers.company_website_domain`, `current_employers.title`, `current_employers.seniority_level`, `headline`, `region`, `num_of_connections`, `years_of_experience_raw`. **Operators:** `(.)` = fuzzy contains (default), `[.]` = substring, `=`, `!=`, `in`, `not_in`, `>`, `<`, `=>`, `=<`. Person search also supports `geo_distance` for `region`. **Range filtering:** There is NO range operator like `[100..500]` or `between`. To filter a numeric range, use TWO separate filter conditions with `>` and `<` (or `=>` and `=<`): ```json [ { "filter_type": "employee_metrics.latest_count", "type": ">", "value": "100" }, { "filter_type": "employee_metrics.latest_count", "type": "<", "value": "500" } ] ``` **Headcount filtering:** For headcount, prefer `employee_count_range` (string enum like `"51-200"`, `"201-500"`) with the `in` operator when exact buckets work. Use `employee_metrics.latest_count` with `>` / `<` only when you need precise numeric boundaries. Note: `employee_metrics.latest_count` is valid for `sorts` and `filters`, but `employee_count_range` uses string enum values. ### Examples ```bash deepline tools execute crustdata_companydb_autocomplete --payload '{"field":"linkedin_industries","query":"software","limit":5}' ``` ```bash deepline tools execute crustdata_companydb_search --payload '{"filters":[{"filter_type":"linkedin_industries","type":"(.)","value":"software"},{"filter_type":"hq_country","type":"=","value":"USA"}],"limit":5}' ``` ```bash deepline enrich --input accounts.csv --output accounts.csv.out.csv --with '{"alias":"company_lookup","tool":"crustdata_companydb_autocomplete","payload":{"field":"company_name","query":"{{Company}}","limit":1}}' ``` -
databricks.md 3.5 KB
# Databricks guidance Use `databricks_get_semantic_layer` before choosing semantic table, metric, dimension, or filter names. Prefer `databricks_run_semantic_query` for governed analytics and `databricks_run_query` only when the user supplies SQL or explicitly needs SQL-level control. When the customer already governs business measures in a Unity Catalog Metric View, use `databricks_import_metric_view` with its default preview mode. Review the generated YAML before using `save: true`. Metric View measures are mapped to Databricks `MEASURE(...)` expressions; parameterized Metric Views must be queried directly as table-valued functions. Direct queries are read-only. Mutating SQL is unsupported because Statement Execution has no submission idempotency key. Use Databricks named parameter markers (`:customer_id`) with `parameters`; never interpolate untrusted values into SQL. Results are bounded and may be truncated. For large downstream datasets, use the Play dataset path, which performs count and page queries without loading the full result into agent context. ## Connect Databricks In Databricks, open **SQL Warehouses**, select the warehouse Deepline should use, and open **Connection details**. Map the values into Deepline as follows:  | Databricks value | Deepline field | What to enter | | --------------------- | ------------------------------ | --------------------------------------------------------------------------------- | | Server hostname | Workspace URL | Prefix the hostname with `https://`. Do not include a path. | | HTTP path | SQL warehouse ID | Enter only the value after `/sql/1.0/warehouses/`. | | Personal access token | Personal access or OAuth token | Paste the token once. Deepline stores it as a secret. | | Catalog | Default catalog | Optional. Use the Unity Catalog catalog that contains the governed data. | | Schema | Default schema | Optional. Use the schema Deepline should resolve unqualified table names against. | Workspace ID, JDBC URL, and the displayed OAuth URL are not required for a token connection. For production automation, prefer a Databricks service principal with OAuth M2M credentials and least-privilege `CAN USE` access to the SQL warehouse plus the required Unity Catalog grants. Do not enter both a token and OAuth client credentials. For PAT authentication, open **Settings → Developer → Access tokens** and select **Generate new token**. Copy the token when Databricks shows it; the value is displayed only during creation.  After saving, use **Test** in the Deepline integration row. A successful test returns the Databricks user, current catalog, and current schema used for subsequent queries. Databricks references: - [SQL warehouse connection details](https://docs.databricks.com/aws/en/integrations/compute-details) - [Create a personal access token](https://docs.databricks.com/aws/en/dev-tools/auth/pat) - [Authorize a service principal with OAuth M2M](https://docs.databricks.com/aws/en/dev-tools/auth/oauth-m2m) -
dataforseo.md 950 B
# DataForSEO Use DataForSEO when you need broad SEO, SERP, content analysis, backlink, or keyword datasets directly from the provider API. ## Auth - DataForSEO upstream uses HTTP Basic auth. - Store `DATA_FOR_SEO_API_KEY` as either a raw `login:password`, a pre-encoded base64 token, or a full `Basic ...` header value. ## Execution notes - Generated POST tools send the payload as a single-task array because DataForSEO task/live endpoints are array-based. - Generated GET tools pass top-level fields as query parameters. - Pricing metadata is scraped from DataForSEO pricing pages and may be formula-based rather than fixed per call. ## Generated surface - Most tool docs, fixtures, sample outputs, mocks, and catalog metadata are generated by `packages/integrations/dataforseo/dev/generate-dataforseo-provider.ts`. - If the docs or schemas drift, update the scraper/generator and regenerate instead of manually editing generated artifacts. -
datagma.md 4.4 KB
# Datagma Workflow Guidance Datagma is strongest when you need real-time enrichment rather than a static contact database. It is especially useful for direct mobile numbers, international coverage, and job-change validation. ## When to use Datagma - Use `datagma_full_enrichment` when you have a strong identifier such as a LinkedIn URL, a professional email, or a domain-backed full name. - `datagma_enrich_person` remains a compatibility alias for the flat legacy response. `datagma_enrich_company` returns the native Datagma response body; use its canonical Deepline getters for company name and domain. - Use `datagma_find_email` when you only need a verified work email and want a narrower, cheaper workflow than full enrichment. - Do not use Datagma for personal-email-only waterfall steps. Public docs expose verified work email and full enrichment, not a personal-email-only endpoint. - Use `datagma_search_phone_numbers` when you already have an email or social URL and want direct mobile numbers. - Use `datagma_job_change_detection` before outreach refreshes when you need to confirm whether a contact is still at the same company. - Use `datagma_find_people` to source up to 10 people by title inside a target company. ## Input strategy 1. LinkedIn URL or company domain 2. Professional email 3. Full name plus company context 4. Company-name-only lookups only when nothing stronger is available Datagma’s own docs emphasize that LinkedIn URL and domain-backed inputs are the most reliable. Prefer those over plain company-name searches. ## Billing behavior to remember - Mobile phone lookups are substantially more expensive than verified email lookups. Prefer `datagma_find_email` when a work email is all you need. - `datagma_full_enrichment`, `datagma_job_change_detection`, and `datagma_search_phone_numbers` report vendor usage in the response. Deepline uses that vendor-reported value rather than guessing. - Where vendor-reported usage is absent, Deepline falls back to endpoint-specific, documented success fields rather than treating every non-empty profile as a personal-email hit. - Catch-all email results are free. - Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. ## Endpoint guidance ### `datagma_find_email` Use for: - verified work-email lookup from name + company context Best inputs: - `firstName` + `lastName` + `companyDomain` - or `fullName` + `companyDomain` - optionally `linkedInSlug` when you have the company LinkedIn slug Canonical Deepline snake_case column names map straight through, so CSV columns named `first_name` / `last_name` / `full_name` / `company_name` / `company_domain` / `domain` are accepted as input aliases for their camelCase equivalents (the bare `domain` column is treated as the company web domain). ### `datagma_full_enrichment` Use for: - person or company enrichment - firmographics plus person details in one pass - real-time phone/email/company expansion Best inputs: - `data` set to a LinkedIn URL or professional email - `fullName` or `firstName` + `lastName` only when paired with `data` Important: - `phoneFull=true` should be reserved for cases where you do not already have a social profile or email, matching Datagma’s docs guidance. ### `datagma_job_change_detection` Use for: - validating whether a contact is still at the same company Best inputs: - `fullName` + `companyName` - add `jobTitle` when the contact name may be ambiguous ### `datagma_find_people` Use for: - prospecting inside one company by role title Best inputs: - `currentJobTitle` - plus one of `linkedinId`, `domain`, or `currentCompanies` - add `countries` when known to improve relevance The action retains the documented contract while Deepline translates it to Datagma's current employee-finding endpoint. ### `datagma_search_phone_numbers` Use for: - direct mobile-number search from an email or profile URL Best inputs: - both `email` and `username` when you have them - Datagma explicitly recommends passing both together when possible ## Internal-only endpoints Datagma also exposes reverse-email, reverse-phone, and Twitter lookup endpoints. They remain internal in this repo because the current public docs do not disclose standalone pricing for them. Do not move them to the public tool surface until pricing is verified against live credentials or direct vendor confirmation. -
deeplineagent.md 1.9 KB
Use `ai_inference` for plain text or structured-output model calls with no tool use. Model IDs are case-sensitive. Use the exact `id` string from the live [AI Gateway model catalog](https://ai-gateway.vercel.sh/v1/models); the current Deepline default is `openai/gpt-5.6-luna`. The `tools describe` CLI also shows the selected model metadata and provider option schema: `deepline tools describe ai_inference --model openai/gpt-5.6-luna --json`. For inference and `deeplineagent`, read `toolResponse.raw.result.text` for text, `toolResponse.raw.result.object` for optional structured data, and `toolResponse.raw.extracted_json` for the compatibility JSON field. For `ai_evaluate`, use `toolResponse.raw.result.answers` and `toolResponse.raw.extracted_json`. Use `deeplineagent` when the task benefits from streaming output and tool use across the current whitelist: `serper_google_search`, `exa_search`, `firecrawl_scrape`, `firecrawl_map`, `firecrawl_crawl`, and `bash`. For research tasks, prefer an adaptive loop: cheap Serper search first, synthesize, then only run targeted Exa follow-up searches if key gaps remain. When using `exa_search`, prefer `type: "auto"` with `contents.highlights` and no `contents.summary`; highlights are token-efficient snippets, while summaries add an extra AI pass. Use Firecrawl only after you know the target site or section. Prefer `firecrawl_scrape` for one known URL, `firecrawl_map` for URL inventory before crawling, and keep `firecrawl_crawl` tightly scoped with explicit low `limit`, `includePaths`/`excludePaths`, or `maxDiscoveryDepth`. Firecrawl work scales with discovered pages, crawled pages, and requested modifiers, so do not run broad speculative crawls. When `bash` is enabled, `/refs/prompts.json` is available as a lazily loaded reference file for GTM prompt-template lookup. Prefer Deepline-native tools over freeform bash when structured provider actions already exist. -
deepline_native.md 9.2 KB
# Deepline Native — Agent Guidance ## Operation Selection | Goal | Operation | | ----------------------------------------- | ---------------- | | Find contacts at a company | `prospector` | | Enrich a single contact | `enrich_contact` | | Look up phone numbers | `enrich_phone` | | Enrich a company record | `enrich_company` | | Detect job changes (preferred) | `job_change` | | LinkedIn lookup — dropleads fallback only | `search_contact` | ## Provider Positioning - **`job_change`**: preferred job-change provider — charges only on confirmed moves. - **`search_contact`**: secondary people search. **Not free** — `0.56` Deepline credits per contact returned on successful calls. Zero returned contacts are free. Deepline `422` schema validation errors are rejected before provider execution and should not bill. **Dropleads is the default people search and is free per call.** Use `search_contact` only when dropleads fails or is unavailable. Not yet tested enough to be the primary path. - **`prospector` / `enrich_contact`**: use when Dropleads coverage is insufficient for the target segment. - **`enrich_company`**: `0.98` Deepline credits per call. Use when firmographic data is required and other sources have been exhausted. ## Key Behaviors - Rate budget split: `search_contact` uses the dedicated search key/budget (`60 RPM`). - Rate budget split: `prospector`, `enrich_contact`, `enrich_phone`, `enrich_company`, `job_change`, and related finder calls use the enrichment key/budget (`200 RPM`). - When planning `deepline enrich` waterfalls, do not treat `search_contact` and the enrichment-style Waterfall actions as one shared bucket. ### Launcher operations (prospector, enrich_contact, enrich_phone, enrich_company) - These are async but the executor waits for completion and returns the final payload. - The result includes `job_id` if you need to re-fetch later via finder operations. - Finder endpoints (`*_finder`) are available for explicit polling by `job_id`. ### Synchronous operations (job_change, search_contact) - Return results immediately. No job_id, no polling needed. ## Operation-Specific Notes ### prospector - Requires one company identifier (domain, company_name, or linkedin) AND one title filter. - Keep title filters specific — e.g. `vp sales OR director of sales` not just `sales`. - Use `title_filters` (ordered array) to cascade: fill C-suite first, then VP, then Director. - Use `location_countries` not `location_name` when filtering by country — exact names required. - `verified_only: false` returns catch-all emails in addition to safe-to-send. - `include_phones: true` runs phone enrichment on all returned contacts (adds cost). ### enrich_contact - Best input: `linkedin` URL + `domain`. - Raw tool accepts exactly one identity strategy: `email`, `linkedin`, `full_name + domain`, or `first_name + last_name + domain`. - Name-only input is not valid. If you know the account domain, prefer `first_name + last_name + domain` over LinkedIn because it anchors the result to that company. - Do not use for bulk enrichment — run one identity at a time. ### job_change - Preferred job-change provider. Only charges `1.96` Deepline credits when status is `moved`. - Field names match the API exactly: `company_domain`, `professional_email`, `contact_linkedin`, etc. Do NOT use `email`, `linkedin`, `domain` — those are wrong field names for this operation. - Best coverage combo: `company_domain` + `contact_linkedin`. - Minimum viable: `professional_email` alone. - When status is `left`, `no_change`, or `unknown` — no charge, person object may be empty. - Check `output.job_change_status` for the result. ### search_contact - **Not the default people search — dropleads is.** Use as a fallback when dropleads fails or returns no results. - Uses the dedicated Waterfall search key/budget (`60 RPM`), separate from the higher-throughput enrichment key. - **Pricing: `0.56` Deepline credits per contact returned (post-deduct, billed on success). Zero returned contacts are free. Deepline `422` schema validation errors, such as `title_lists[0].titles` not being an array, happen before provider execution and should not bill.** A `page_size: 10` call that returns 10 contacts costs `5.6` credits — keep `page_size` small (`1-3`) for targeted lookups. - Synchronous. Returns LinkedIn URLs only — email and phone are always redacted. - Treat it as a company-scoped LinkedIn candidate finder, not a clean org-chart API. It is good at surfacing plausible current people at a company, but broad title queries can still return adjacent or support roles. - Follow up with `enrich_contact` to get email/phone for returned LinkedIn URLs. - Supports pagination: `page_number` and `page_size` (default 10, max 250 per page). - Always include `domain` when you can. That is the strongest company anchor and produced the best live results. - Prefer `title_filters` as `{name, filter}` objects. Use `title_lists` for exact title matching. Legacy string arrays are normalized into a single `title_lists` entry, but raw object form is clearer and more reliable. - Best live pattern: function-specific leadership queries such as `VP Sales OR Head of Sales OR Director of Sales` or `VP Engineering OR Head of Engineering OR Director of Engineering`. - Risky pattern: broad executive/founder searches such as `CEO OR Founder OR Co-Founder`, which can return noisy founder-adjacent or regional-entity matches. - Live API `seniorities` support is narrower than our higher-level plays. Confirmed safe values: `Director`, `Manager`, `Entry`, `Senior`, `Partner`. - Legacy `seniority`/portable values are normalized on the raw tool path where possible. Unsupported values such as `C-Level` are dropped rather than forwarded upstream. - Keep `page_size` small for targeted lookups, usually `1-3`. - Results are at `output.persons` — an array of person objects with `linkedin_url`. - Inspect the returned `title` and current experience before trusting rank 1. Good queries return strong candidates; weak queries often return either `0 results` or obvious near-misses. ### enrich_company - Pre-reserves `0.98` Deepline credits per launch. - Input: domain is most reliable; linkedin also works. - Result is nested under `output.company.*` — not top-level. ## Common Patterns ### Prospect + Enrich flow 1. `prospector` — find contacts at target companies with title filters 2. `enrich_contact` — get verified email for specific LinkedIn URLs found 3. (Optional) `enrich_phone` — get phone for priority contacts ### Job change signal flow (on CRM contacts) 1. `job_change` with `company_domain` + `professional_email` 2. If `job_change_status === "moved"`: use `person.company_domain` and `person.linkedin_url` to target at new company 3. If `job_change_status === "left"` or `"no_change"`: no action needed, no charge ### LinkedIn lookup fallback flow 1. Try dropleads first (`dropleads_search_people`) — it is the default. 2. If dropleads fails or returns no results, fall back to `search_contact` with domain + title_filters. 3. Follow up with `enrich_contact` on the returned `linkedin_url` to get email. ### CLI quick checks ```bash deepline tools get deepline_native_job_change deepline tools execute deepline_native_job_change --payload '{"company_domain":"stripe.com","professional_email":"jane@stripe.com"}' deepline tools execute deepline_native_search_contact --payload '{"domain":"stripe.com","title_filters":[{"name":"eng","filter":"VP Engineering OR Head of Engineering"}],"page_size":5}' deepline tools execute deepline_native_search_contact --payload '{"domain":"hubspot.com","title_filters":[{"name":"sales-leadership","filter":"VP Sales OR Head of Sales OR Director of Sales"}],"page_size":3}' deepline tools execute deepline_native_search_contact --payload '{"domain":"openai.com","title_filters":[{"name":"eng-leadership","filter":"VP Engineering OR Head of Engineering OR Director of Engineering"}],"page_size":3}' deepline tools execute deepline_native_enrich_company --payload '{"domain":"stripe.com"}' ``` ### `deepline enrich` usage ```bash deepline enrich --input contacts.csv --output contacts.csv.out.csv \ --with '{"alias":"job_change","tool":"deepline_native_job_change","payload":{"company_domain":"{{domain}}","professional_email":"{{email}}"}}' ``` ## Anti-Patterns to Avoid - Do not default to `search_contact` for people search — use dropleads first. - Do not assume portable play-style seniorities like `C-Level` are valid on the raw `search_contact` tool. - Do not use broad founder/CEO filters when you really want a functional leader at a company; they can produce noisy candidate sets. - Do not use `email`/`linkedin`/`domain` field names for `job_change` — use the correct API names (`professional_email`, `contact_linkedin`, `company_domain`). - Do not use `search_contact` expecting email or phone — those are always redacted. - Do not loop finder endpoints more than 20 times — jobs that don't complete in ~5 minutes have failed. - Do not run `prospector` without a title filter — results will be unbounded. - Do not read `enrich_company` result at top level — data is nested under `output.company.*`. -
discolike.md 3.6 KB
# DiscoLike Integration Guide Use DiscoLike for website-first company discovery and domain intelligence. ## Best entry points 1. `discolike_count` when you want a fast volume estimate for supported search filters like phrase, category, geography, technology, or business model before paying to retrieve a large result set. 2. `discolike_discover` when you have seed domains or a natural-language ICP. 3. `discolike_bizdata` when you already know the domain and need a firmographic profile. 4. `discolike_match` when you start from a company name and need the best matching domain. 5. `discolike_search_indexed_contacts` when you want indexed contacts from DiscoLike's contact dataset at known company domains or matching contact/company filters. 6. `discolike_generate_candidate_contacts` when you want ContaGen-style open-web research to generate candidate contacts at known company domains using DiscoLike-configured BYOK model and search-provider integrations. 7. `discolike_run_company_research` when you want Claygent-style arbitrary company research: run one prompt per domain against company context and optional web search. 8. `discolike_vendors`, `discolike_publiclink`, `discolike_subsidiaries`, and `discolike_redirects` for relationship mapping. ## Segment behavior `discolike_segment` is public and synchronous from the agent perspective: it submits DiscoLike's async segment job, polls the provider status endpoint, and returns the completed segment rows when available. If the sync wait times out, call `discolike_get_segment_status` with the returned `task_id` to retrieve the provider result later. This is a free read-only job-ID action for tasks created by the same organization. Billing separately tracks accepted provider work until terminal usage is known; no result cache resumes the task. ## Contact generation behavior `discolike_search_indexed_contacts` searches DiscoLike's indexed contact/persona data. `discolike_generate_candidate_contacts` is the ContaGen path: it submits `POST /contacts/discover/generate`, then polls DiscoGen status until terminal results are available or the caller's wait budget expires. Use it only when candidate open-web contact discovery is acceptable. Treat generated `email`, `linkedin_url`, phone, and identity fields as candidates until validated. `discolike_run_company_research` is the Claygent-like path: it submits `POST /discogen/process`, runs an arbitrary prompt against each company domain, and polls DiscoGen status for final results. DiscoGen/ContaGen are BYOK from DiscoLike's perspective: they use the DiscoLike-configured LLM and search-provider integrations and may return a provider-side `estimated_cost` for model/search usage. For Deepline-managed DiscoLike execution, those configured model/search integrations are Deepline-managed upstream costs, so returned `estimated_cost` is reflected in Deepline billing. If the caller omits `integration_id`, Deepline uses the cheapest verified managed LLM config, OpenAI `gpt-5-nano`; if the caller omits `search_context_size`, Deepline uses `low`. Caller-selected model/search overrides, high search context, and `web_search: true` are blocked on the managed shared-key path. Website/profile context modes cost more than domain mode because they add company-context usage on top of the task launch. ## Pricing caveat DiscoLike usage is billed per query plus per net-new record retrieved. Records cached within the provider account for 90 days are free on repeat retrieval upstream, but Deepline uses stable customer-facing modeled pricing regardless. Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. -
dropleads.md 8.7 KB
# Dropleads Playbook Use Dropleads as a two-phase flow: low-cost contact discovery first, paid enrichment second. Do not use Dropleads people search as the first step for account discovery. ## 1) Start with low-cost discovery - Use `dropleads_get_lead_count` to size the audience before any paid call. - Use `dropleads_search_people` to inspect masked contacts and validate ICP filters (free). - Use `dropleads_search_people` after you already have target account domains, by passing `filters.companyDomains`. It is a contact search primitive, not a dependable way to discover target accounts. - Do not build joins or account-discovery flows that depend on every returned lead having `companyDomain`. Treat returned `companyDomain` as optional; if you need account domains as the source of truth, use a company-native search/enrichment tool first. - Tighten filters until sample rows clearly match role, industry, and geo expectations. - Key filter fields: `filters.jobTitles`, `filters.seniority` (VP/Director/Manager/Senior/Entry/Intern), `filters.industries`, `filters.departments`, `filters.companyDomains`, `filters.employeeRanges`, `filters.personalCountries`, `filters.personalStates`, `filters.personalCities`, `filters.organizationCountries`, `filters.organizationStates`, `filters.organizationCities`, `pagination.page`, `pagination.limit`. Use `personal*` for the contact's location and `organization*` for company HQ; a person's location is not necessarily their employer's HQ. Use title terms like `CEO`, `CTO`, or `Founder` for C-level searches; do not pass `C-Level` as a Dropleads seniority value — Dropleads' own API docs list it, but live validation rejects it. `filters.seniorityExclude` and `filters.departmentsExclude` take the same exact values as their include twins. ### Filter best practices All Dropleads filters nest under the `filters` object. Pagination nests under `pagination`. The canonical payload shape: ```json { "filters": { "companyDomains": ["microsoft.com"], "jobTitles": ["CTO", "VP Engineering"], "seniority": ["VP", "Director"], "personalCountries": { "include": ["United States"] } }, "pagination": { "page": 1, "limit": 25 } } ``` **Quick reference — correct filter keys:** | Filter | Correct key | Why | | ----------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Company | `filters.companyDomains` | Exact domain match for known accounts. Prefer this when you already have domains; do not rely on people-search results to discover a complete account-domain list. `companyNames` does fuzzy substring matching — "Microsoft" pulls in unrelated businesses. | | Person geo | `filters.personalCountries`, `filters.personalStates`, `filters.personalCities` | Filters the contact's location. Each field uses an `{ "include": [...], "exclude": [...] }` object. | | Company geo | `filters.organizationCountries`, `filters.organizationStates`, `filters.organizationCities` | Filters the company HQ, not the contact's location. Each field uses an `{ "include": [...], "exclude": [...] }` object. | | Seniority | `filters.seniority`, `filters.seniorityExclude` | Exact values only: `VP`, `Director`, `Manager`, `Senior`, `Entry`, `Intern`. Use `jobTitles` terms like `CEO`, `CTO`, or `Founder` for C-level searches. | | Department | `filters.departments`, `filters.departmentsExclude` | Exact values only: `Engineering`, `Sales`, `Marketing`, `Operations`, `Finance`, `HR`, `Product`, `Customer Success`, `Legal`, `IT`. | | Industry | `filters.industries` | Exact strings from Dropleads. Pilot with a broad search first when unsure. | ### Exclude filters use the same vocabulary as their include twin `seniorityExclude` and `departmentsExclude` take the exact same values as `seniority` and `departments`. Prime-DB itself silently ignores an unrecognized value in either exclude list: the exclusion you asked for never happens, the call still returns 200, and the result set still shifts because the filter key is present. Deepline rejects unknown values with a 422 instead of letting a wrong answer look like a valid one. Also note that sending `seniorityExclude` at all drops leads that carry no seniority in Prime-DB, so it narrows results beyond the levels you name. On a `sephora.com` sample: 1514 leads with no seniority filter, 1058 with all six levels included, 94 with all six excluded. ### Geo filters are best-effort, not verified Dropleads geo filters (`personalCountries` / `personalStates` / `personalCities` and `organizationCountries` / `organizationStates` / `organizationCities`) match against **self-reported, LinkedIn-sourced location text** — they are not verified against the contact's actual location. Treat them accordingly: - **City-level is the loosest match and leaks.** `personalCities` can return contacts whose stated city loosely matches even when their real location differs, and non-US contacts can appear under a US-city filter (e.g. a Bulgarian contact surfacing under `personalCities: San Francisco` + `personalCountries: United States`). Country/state are more reliable. - **Person vs. company location are different fields.** `personal*` filters the contact's own location; `organization*` filters the company HQ. Don't conflate them — filtering a remote employee by company HQ city (or vice versa) drops or leaks legitimate matches. - **Verify geo when precision matters.** Combine `personalCountries`/`personalStates` with `personalCities`, then post-filter the returned leads on their `country`/`state`/`city` (and exclude obvious mismatches) before trusting the result or spending on enrichment. Do not assume the filter alone guarantees the geo. ## 2) Escalate paid calls only for shortlisted targets - Run `dropleads_email_finder` for contacts that passed the discovery pass. - Run `dropleads_mobile_finder` only when phone is required for the workflow. - Keep pilots small first, then scale after quality checks pass. ## 3) Gate outbound with verifier status - Treat `invalid`, `catch_all`, and `unknown` as non-send by default. - Treat `valid` as the only status that passes automatic send gates. - Respect `credits_charged` in responses for post-execution billing accuracy. ## 4) Practical sequencing 1. Count segment (`dropleads_get_lead_count`). 2. Sample segment (`dropleads_search_people`). 3. Pre-score titles with `run_javascript` if looking for a specific profile (e.g. founders, GTM engineers). 4. Retrieve LinkedIn profiles with `harvestapi_get_profile` for structured work history and signals. Use Apify only when native HarvestAPI does not expose the required LinkedIn shape. 5. Extract signals with `run_javascript` from the structured HarvestAPI output (e.g. founder detection, hiring signals). 6. Enrich emails via waterfall (`dropleads_email_finder` first, then other providers). 7. Verify candidate emails (`dropleads_email_verifier` or `leadmagic_email_validation`). 8. Expand only after pilot quality is confirmed. ## 5) Account discovery boundary For account-based pipelines, start with a company-native source that returns account domains as first-class results. Feed those domains into Dropleads via `filters.companyDomains` to find contacts at known accounts. Dropleads may include `companyDomain` on returned people, but it is not guaranteed enough to be the join key that creates the account universe. -
emailbison.md 634 B
Use EmailBison read endpoints to inspect campaigns, sender email accounts, replies, lead state, tags, and warmup state before making writes. Prefer targeted campaign/lead operations over workspace/admin operations. Mutating workspace, token, sender account, webhook, and delete operations require the user's own credential and should be used only when the requested workflow explicitly needs them. Do not expose EmailBison team credit balances as customer-facing provider spend. Account details and balance fields stay internal, and there is no shared Deepline balance monitor for EmailBison because API keys are customer-specific. -
emailguard.md 243 B
# EmailGuard Use only after the workspace connects its own EmailGuard API token. Account and workspace settings, hosted redirects, domains, and tests mutate the customer’s deliverability setup, so make those calls only on explicit request. -
enformion.md 9.3 KB
# EnformionGO Integration Guide Consumer skip-trace API (Galaxy) for finding an individual's **personal contact info** — personal emails and mobile phones — from name + city/state. Tool quick-pick: - **Reverse phone lookup** → `enformion_reverse_phone_search` (ReversePhone; returns everyone associated with a number, including historical associations). Treat results as candidates, not proof of current ownership. - **LinkedIn profile lookup** → `enformion_linkedin_id` (LinkedinID; resolves a LinkedIn profile URL to a person record). - **Property/address lookup** → `enformion_property_v2_search` (PropertyV2; supports free-form, TahoeId, and structured address searches). - **Personal email** → `enformion_person_search` (Person database; returns `emailAddresses[]` flagged `nonBusiness:1`). Do NOT use `enformion_workplace_search` for personal email — it returns other-employer work emails. - **Personal mobile (phone-first)** → `enformion_contact_enrich` (skip-trace, ~53% mobile hit rate). - **Officers of a business** → `enformion_business_search` → officer `tahoeId` → `enformion_person_search` by tahoeId. ## When to use Use `enformion_contact_enrich` when you need to find the personal mobile of a small business owner (restaurant, retail, local services) who has: - No LinkedIn profile - No company domain email - No B2B database presence This is the right tool after you've already extracted the owner's name from a public source (Google search, Yelp, SOS officer records, OpenMart staff). ## Critical gotchas — read before calling ### 1. Strip middle initials from last_name (automatic) The integration automatically strips middle initials. You do NOT need to pre-process — just pass the raw last name. But be aware: - `"K Tarzai"` → bare last = `"Tarzai"` ✓ - `"De La Cruz Castillo"` → bare last = `"Castillo"` ✓ - `"Smith Jr."` → bare last = `"Smith"` ✓ If you want to override this, pass only the bare last word yourself. ### 2. FL SOS officer names (use reverseFlOfficerName helper) Florida SOS returns officer names as `"LASTNAME, FIRSTNAME MIDDLE"`. You must reverse these before calling: - `"SMITH, JOHN EDWARD"` → `"John Smith"` ✓ - `"DE LA CRUZ, MARIA"` → `"Maria De La Cruz"` ✓ The helper `reverseFlOfficerName()` is exported from `actions/enformion-shared.ts`. ### 3. Rate limits No documented rate limit, but runs degrade with >200 concurrent requests. The Deepline rate limiter caps at 10 req/s which is safe for all batch sizes. ### 4. No-result interpretation - `identityScore: 0` = no match found (not billed) - `identityScore: 75–100` = high confidence match - `person.phones[]` empty = found the person but no phone on record ## Response structure ```json { "person": { "name": { "firstName": "Gary", "lastName": "Lincoln" }, "age": "45", "addresses": [...], "phones": [ { "number": "(856) 725-5922", "type": "mobile", "isConnected": true, "firstReportedDate": "...", "lastReportedDate": "..." } ], "emails": [...] }, "identityScore": 100, "isError": false } ``` Extract the mobile: `person.phones.find(p => p.type === "mobile" && p.isConnected === true)?.number` ## Validation After finding a phone via EnformionGO, validate it with `trestle_phone_validation`: - `line_type: "Mobile"` + `activity_score >= 60` = confirmed owner mobile - `line_type: "Landline"` or `line_type: "FixedVOIP"` = store/office phone — discard ## Credentials Requires two custom headers set from stored credentials: - `galaxy-ap-name`: your API key name - `galaxy-ap-password`: your API key password Set via Deepline dashboard → Integrations → EnformionGO, or via `ENFORMION_KEY_NAME` + `ENFORMION_KEY_PASSWORD` environment variables. API endpoint: `https://devapi.enformion.com/Contact/Enrich` ## enformion_reverse_phone_search API endpoint: `https://devapi.enformion.com/ReversePhoneSearch` (`galaxy-search-type: ReversePhone`). Pass `phone`; optional pagination is supported. Reverse Phone returns all people associated with the number, which can include old owners, employees, or prior residents. ## enformion_property_v2_search API endpoint: `https://devapi.enformion.com/PropertyV2Search` (`galaxy-search-type: PropertyV2`). Use `free_form_search`, `tahoe_id`, or structured address fields. --- ## enformion_person_search — PERSONAL emails (use this, not workplace_search) ### When to use Use `enformion_person_search` whenever you need a person's **personal email** (and/or phone). This is the Galaxy "Person Search" database (`galaxy-search-type: Person`). Unlike Workplace Search — which returns the owner's *other-employer* work emails 97% of the time — Person Search returns the individual's personal addresses (gmail/yahoo/comcast), each flagged `nonBusiness: 1`. Two lookup modes: - **By name + city/state** — the common path. `enformion_person_search({ first_name, last_name, city_state })`. - **By tahoe_id** — exact-person lookup. Pass `{ tahoe_id }` to pin one identity (e.g. an officer tahoeId returned by `enformion_business_search`). When `tahoe_id` is set, the name/location fields are ignored. Middle initials and compound surname prefixes are stripped automatically (same silent-zero-result bug as Contact/Enrich). ### Recommended flow for business officers (vendor-recommended) To get the personal contact info of a business's officers/agents: 1. `enformion_business_search({ name, city_state })` → business record with `officers[]` / agents, each carrying a `tahoeId`. 2. For each officer, `enformion_person_search({ tahoe_id })` → that person's `emailAddresses[]` + `phoneNumbers[]`. **Entitlement note:** Business Search is a *separately entitled* Galaxy product. If the access profile is not provisioned for it, the call returns `isError: true` ("Access Profile does not permit client to call Business Search."). When that happens, **fall back to person-search-by-name** using the officer name you already resolved (from Google/Yelp/SOS) — Person Search is entitled and returns the same personal contact data. (As of 2026-06-09 the `aeroailabs` profile has Person Search but not Business Search.) ### Response structure (Person Search) ```json { "persons": [ { "tahoeId": "G-2492258155993150029", "fullName": "Maria Delcarmen Castillo", "name": { "firstName": "Maria", "lastName": "Castillo" }, "age": 49, "emailAddresses": [ { "emailAddress": "mcastillo12692@gmail.com", "emailOrdinal": 1, "isPremium": true, "nonBusiness": 1 } ], "phoneNumbers": [ { "phoneNumber": "(305) 555-0142", "phoneType": "Wireless", "isConnected": true } ], "addresses": [ { "fullAddress": "...", "city": "Miami", "state": "FL" } ] } ], "isError": false } ``` Extract the best personal email: `persons[0].emailAddresses[0].emailAddress` (prefer entries with `isPremium: true`). No result: `persons` is an empty array. ### Billing Billed per match (`post_deduct`) only when the top match has a personal email or a connected phone. No charge on empty / no-match. API endpoint: `https://devapi.enformion.com/PersonSearch` (`galaxy-search-type: Person`). Business Search endpoint: `https://api.galaxysearchapi.com/BusinessV2Search` (`galaxy-search-type: BusinessV2`, body uses camelCase `businessName` + `addressLine2`). Note the different host from the other endpoints — Business Search is served from `api.galaxysearchapi.com`, not `devapi.enformion.com`. --- ## enformion_workplace_search ### When to use Use `enformion_workplace_search` as a **fallback** when `enformion_contact_enrich` returns `identityScore: 0` or an empty phones array. It queries a separate B2B employment profile database and has a different rate-limit bucket, so it does not compete with Contact/Enrich capacity. `city_state` is optional here (unlike Contact/Enrich where it is required). Omitting it broadens the match at the cost of potentially more ambiguous results. ### Critical warning: emailAddresses[] `emailAddresses[]` in Workplace Search results are almost always the owner's OTHER employer (a corporate day job, a franchise group, etc.) and NOT the restaurant's email. In practice: - 97% of returned email addresses belong to a different employer. - Only use an email address if the domain matches the target restaurant's domain (e.g., `gary@lincolnautogroup.com` is useless if you want `gary@lincolndiner.com`). ### Rate-limit bucket Workplace Search uses a **separate** rate-limit bucket from Contact/Enrich. Both can run concurrently without reducing each other's throughput. The Deepline rate limiter treats them as independent pools. ### Response structure ```json { "workplaceRecords": [ { "fullName": "Gary Lincoln", "firstName": "Gary", "lastName": "Lincoln", "professionalTitles": "Owner", "phoneNumbers": ["+18567255922"], "emailAddresses": ["gary.lincoln@someemployer.com"], "currentEmployment": [ { "employer": "Lincoln Auto Group", "jobTitle": "Owner", "level": "Owner", "department": "Management" } ] } ] } ``` Extract phone: `workplaceRecords[0]?.phoneNumbers?.[0]` No result: `workplaceRecords` will be an empty array `[]`. ### Billing Billed per match (`post_deduct`) only when a workplace record with a phone or email is returned. No charge on empty / no-match. -
enigma.md 1.7 KB
# Enigma Card-spend revenue for US business locations. The only source in the Deepline catalog that prices a **private** SMB or multi-location group. ## When to use it - Sizing a private business or multi-location group that files no public financials - Ranking local-services accounts by revenue per location instead of review count - Detecting decline (negative growth) as an outbound trigger - Segmenting by average ticket, which separates business models better than location count does ## Contract notes `enigma_brand_revenue_search` resolves a brand name to its operating locations and returns per-location card metrics plus group-level aggregates. **Always pass `state`.** Enigma matches brand names nationwide, so an ungated lookup mixes unrelated operators that happen to share a name. `state` is applied as a post-response filter on the resolved address; Enigma's own `searchInput.address` is a match hint, not a filter, and returns zero results when supplied alone. ## Reading the numbers **Use `median_yoy_growth`, never the mean.** Mean growth is corrupted by new locations entering the panel. A 10-location group in the field test showed +461% mean growth driven entirely by one ramping new shop at +3915%; its median was a sober +7%. Growth values are ratios, not percentages: `0.0721` means +7.21%. **`revenue_per_location_median` beats the average** for groups with a wide spread between flagship and satellite locations. ## Coverage caveat Card spend covers card transactions only. It **understates invoiced fleet and commercial work**, which is a real share of revenue in verticals like auto repair, HVAC, and plumbing. Treat these figures as directional floors, not audited revenue, and never present them to a customer as their actual revenue. -
exa.md 1.2 KB
Use Exa for web-grounded retrieval, then synthesis. **`type` vs `category`:** `type` controls the search strategy (auto/fast/neural/deep). `category` filters result type (company/people/news/financial report). These are independent params. `"type":"news"` is a 422 error — use `"category":"news"` for news results. - In AI-column workflows, instruct the model prompt to use Exa retrieval explicitly for website-derived tech stack and on-site signals. - Use direct Exa tool calls when you need tighter provider controls or auditable step-by-step retrieval outside AI-column orchestration. - For auditable outputs, run `exa_search`/`exa_contents` first and synthesize after inspecting citations. - Use focused queries and small `numResults` during pilots, then widen only if coverage is low. - Treat `exa_answer` as the summarization layer, not the first retrieval step, when precision matters. ```bash deepline tools execute exa_search --payload '{"query":"series b devtools companies united states","numResults":5,"type":"fast"}' ``` ```bash deepline tools execute exa_contents --payload '{"urls":["https://example.com"],"text":true}' ``` ```bash deepline tools execute exa_answer --payload '{"query":"Summarize the top GTM signals from these results","text":true}' ``` -
findymail.md 880 B
# Findymail Use Findymail for verified B2B email lookup, email verification, phone lookup, company enrichment, similar-company discovery, technology lookup, and Intellimatch lead-list workflows. - Prefer `findymail_find_from_name` when you have a person name plus company domain. - Prefer `findymail_find_from_business_profile` or `findymail_reverse_email_lookup` when the input is a LinkedIn/profile URL or an existing email. - Use `findymail_search_technologies` before `findymail_lookup_technologies_by_domain` when the caller wants to filter by a technology name. - Intellimatch is asynchronous: call `findymail_search_leads`, poll `findymail_get_export_status`, then retrieve rows with `findymail_get_results`. - Mutating list, exclusion-list, and signal-monitor operations require the caller's own Findymail credential so agents do not mutate the shared provider account. -
firecrawl.md 2.4 KB
# Firecrawl — Agent Guidance ## Action selection - **Single page** → `firecrawl_scrape`. Returns markdown by default. - **Web search + content** → `firecrawl_search`. Replaces Google search + individual page scraping in one call. - **Site discovery** → `firecrawl_map` first to enumerate URLs, then `firecrawl_batch_scrape` the ones you need. - **Full site crawl** → `firecrawl_crawl_params_preview` to estimate cost, then `firecrawl_crawl`. - **Known URL list** → `firecrawl_batch_scrape`. More efficient than individual scrapes. - **Structured extraction** → `firecrawl_extract` with a JSON schema or natural language prompt. - **Complex web tasks** → `firecrawl_agent` with a natural language instruction. The agent navigates pages autonomously. ## Budget awareness - Cost scales with pages processed: scrape, crawl, batch scrape, and map are billed per page, search is billed per result, and agent and extract are dynamic. - Scrape options increase per-page cost. PDF parsing, JSON format, Enhanced Mode, and Zero Data Retention each add to the base page cost, and they stack. - Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. - Firecrawl can charge when infrastructure processes a request even if the target returns 403/404. Avoid blind retries of blocked URLs; inspect `metadata.statusCode`. - Crawl defaults to `limit: 10000` and Firecrawl preflights available balance against that limit. Always pass an explicit lower `limit` unless a 10000-page crawl is intentional. - `crawl_params_preview` is free and shows estimated usage before committing. ## Async operations - `crawl`, `batch_scrape`, `agent`, and `extract` are async. They return a job ID immediately. - The action handler polls automatically for up to 5 minutes and returns results when ready. - For non-blocking usage, use the corresponding status-check action (`get_crawl_status`, etc.) to poll manually. - Cancel long-running jobs with the cancel actions when results are no longer needed. ## Format recommendations - Use `markdown` format for LLM consumption (default). - Use `html` when you need the raw DOM structure. - Use `links` to extract all hyperlinks from a page. - Use `screenshot` when visual layout matters. ## Rate limits - Standard rate limit is 15 requests/second. - Batch and crawl operations are rate-limited server-side; the API handles queuing. -
fireflies.md 988 B
# Fireflies.ai Start with `fireflies_transcripts` to discover meeting IDs. Use `limit` and `skip` to paginate; Fireflies allows at most 50 transcripts per request. Use `keyword`, date bounds, organizers, participants, channel, host, or `mine` to narrow the list. Use `fireflies_transcript` when you need the full meeting record. It includes sentences, summaries, analytics, attendee data, `transcript_url`, `audio_url`, and `video_url`. Fireflies exposes recordings through transcript media URLs rather than a separate videos endpoint. Use IDs returned by list actions for channel, bite, AskFred, live-meeting, sharing, and update actions. Treat delete, upload, share, role, AskFred, and meeting-update actions as writes: confirm user intent and exact IDs before execution. `fireflies_rule_executions_by_meeting` and `fireflies_audit_events` may require an enterprise Fireflies plan. All actions require a customer-owned Fireflies API key. Deepline does not bill for Fireflies usage. -
forager.md 2.4 KB
# Forager Workflow Guidance Forager has 850M+ B2B person records and is especially strong for verified mobile phone numbers (200M+ database). Prefer Forager over other providers when the goal is phone number discovery. ## Search & Discovery - Always start with totals operations (`forager_person_role_search_totals`, `forager_organization_search_totals`, `forager_job_search_totals`) to estimate result volume at zero credit cost before running paid searches. - Use `forager_person_role_search` for prospecting by role, skills, and company criteria. Search is billed per page, so keep pages bounded while exploring. - Use `forager_organization_search` for company prospecting by industry, size, technology stack, and revenue filters. - Use `forager_job_search` for intent-signal analysis: companies hiring for specific roles indicate growth and budget in those areas. ## Boolean Text Search - Fields like `role_title`, `role_description`, `person_name`, `organization_description` use boolean text search. - **Multi-word phrases MUST be quoted**: `'"VP Sales"'` not `'VP Sales'`. Unquoted phrases cause a parse error. - Supports AND, OR, NOT, and parentheses: `("VP Sales" OR "Director of Sales") NOT intern` ## Filter Fields Use Integer IDs - `person_locations`, `person_skills`, `person_industries`, `organization_locations`, etc. all require **integer IDs**, not name strings. - Retrieve IDs from the lookup operations first (e.g. `forager_industries`, `forager_locations`, `forager_person_skills`). ## Enrichment & Reveals Use `forager_person_detail_lookup` to enrich a known person by `person_id` or `linkedin_public_identifier`. Use `forager_person_detail_reverse_lookup_by_email` when you already know a work/personal email and want to resolve the person. - Use `forager_person_detail_reverse_lookup_by_phone_number` when you already know a phone number and want to resolve the person. - Use `forager_website_detail_lookup` for technographic enrichment on a specific domain. - API docs: https://docs.forager.ai/openapi/api_keys ## Cross-Provider Workflow Tips - When building name-dependent workflows (e.g. email pattern generation), start from a source that returns full names before using Forager for phone/email reveals. - Forager person search results include `person_id` -- save this for subsequent `forager_person_detail_lookup` calls to avoid re-searching. - Check account balance from Forager before large batch operations. -
fullenrich.md 2.1 KB
# FullEnrich Agent Guidance ## Key patterns - **Async submit + async fetch.** `fullenrich_bulk_enrich` and `fullenrich_reverse_email` start background jobs and return an `enrichment_id`. A Play waits synchronously for terminal data, prints the status command before polling, and reports progress about every 15 seconds. Status checks run every 15 seconds. If a run is canceled or reaches its wait limit, the provider job can continue and may still incur Deepline credits; use the logged status command with `fullenrich_get_result` or `fullenrich_get_reverse_result` to check it. - Submit at most 50 contacts or emails per batch. Split larger inputs into multiple calls. - **Use `enrich_fields`** to control what's enriched: `contact.emails` is the cheapest, `contact.personal_emails` costs more, and `contact.phones` is by far the most expensive. - **LinkedIn URL** improves accuracy significantly (5-20% for emails, 10-60% for phones). - **Email status hierarchy:** DELIVERABLE > HIGH_PROBABILITY > CATCH_ALL > INVALID. Use `most_probable_work_email` field for the best result. - **Phone costs 10x email** -- use judiciously and only when explicitly needed. - **Search is synchronous** -- use `fullenrich_people_search` or `fullenrich_company_search` for prospecting. - Status and result reads are free Deepline actions. The accepted batch's Deepline charge settles once from its final usage; do not resubmit a batch that is already running. - **`forceResults=true`** on a direct get-result call returns partial results if enrichment is still running. ## When to use - Best for high-quality email/phone waterfall enrichment with extensive provider coverage (20+ sources). - Search API is good for prospecting by job title, company, location, industry. - Reverse email lookup useful for identifying contacts from email addresses. ## When NOT to use - Don't use for email validation only -- use a dedicated validator (ZeroBounce, LeadMagic validation). - Don't use phone enrichment unless explicitly needed -- it is the most expensive field. - For quick single-provider email lookups, LeadMagic or Prospeo are faster/cheaper. -
generic_http.md 348 B
Use a direct HTTP call when no provider-specific Deepline integration exists. For safe public API calls, validate: - `url` is an absolute public URL. - request method and transport headers are supported. - exactly one body representation (`body_json`, `body_text`, or `body_form_urlencoded`) is set. Prefer a dedicated provider when one exists. -
gong.md 488 B
# Gong Use Gong only with the workspace’s own Access Key and Access Key Secret. Start with read operations for calls, users, and analytics. Write operations can upload recordings, change CRM data, or erase data, so inspect the generated tool description and required identifiers before running them. Gong limits API access by account. Respect a `429` response and its `Retry-After` header. Cursor-based list responses require passing the returned cursor with the same request inputs. -
google_ads_audiences.md 2 KB
# Google Ads Audiences Use these tools for Data Manager v1 audience lifecycle (Customer Match and other ingested user lists) and member uploads. ## Audience kinds and `upload_key_types` Pick the right `upload_key_types` for the data you'll ingest: - `CONTACT_ID` (default) — Customer Match by SHA-256 hashed email, phone, and/or address. - `MOBILE_ID` — IDFA / AAID mobile advertising IDs. Requires `ingested_user_list_info.mobile_id_info.app_id` + `key_space` (`IOS` or `ANDROID`). - `USER_ID` — first-party CRM user IDs. - `PAIR_ID` — publisher/advertiser identity reconciliation IDs. - `PSEUDONYMOUS_ID` — DMP-style pseudonymous IDs. Lists can hold a single key type or a mix. For Customer Match (the common B2B path), use `CONTACT_ID`. ## Account types `account_type` (and `login_account_type`) accepts the Data Manager v1 `accountTypes` enum: - `GOOGLE_ADS` (default — used for Google Ads customer + manager accounts) - `DISPLAY_VIDEO_PARTNER` - `DISPLAY_VIDEO_ADVERTISER` - `DATA_PARTNER` - `GOOGLE_ANALYTICS_PROPERTY` - `GOOGLE_AD_MANAGER_AUDIENCE_LINK` Note: Data Manager v1 does not separate "customer" vs "manager" account-type values like the legacy Google Ads API. Both use `GOOGLE_ADS`. ## Lifecycle - Prefer `google_ads_audiences_create_audience` once per list, then reuse the returned audience ID. - Use `google_ads_audiences_sync_audience_members` with `mode: "replace"` for full refreshes and `mode: "append"` for incremental adds. - Include `login_account_id` when the OAuth user accesses the advertiser through a manager (MCC) account. - Keep Google consent and terms-of-service state explicit. Deepline defaults `terms_of_service_accepted` to true for uploads. - Treat returned `request_ids` as async upload receipts and poll `google_ads_audiences_get_audience_status` for downstream list health. - `membership_life_span_days` is capped at 540 (Google's Customer Match maximum) and is sent as a Data Manager `membershipDuration` Duration string under the hood. -
govfiles.md 1.8 KB
# GovFiles Use company search or officer search to discover US entities across supported jurisdictions. Use company lookup when the jurisdiction code and registry number are already known. For physical locations, submit a local-business batch with 1–500 rows and poll the returned batch id until it is `succeeded` or `failed`; successful results include the complete result document inline. GovFiles uses an `X-API-Key` credential. GovFiles provider charges remain on the connected account, while Deepline separately charges Deepline credits for billable search, direct lookup, and matched local-business operations. ## OpenSOSData migration For an OpenSOSData-style legal-entity lookup with `entity_name` and `state`, use `govfiles_search_companies_v2` with `q` set to the entity name and the state's GovFiles jurisdiction code (for example, `us_de`), then use `govfiles_get_company_v2` with the returned jurisdiction and registry number. Use `govfiles_search_officers_v2` when the workflow needs person-to-company relationships. For OpenSOSData bulk local-business ownership enrichment, use `govfiles_create_local_business_batch` and then `govfiles_get_local_business_batch`. GovFiles accepts 1–500 rows per batch, so a 1,000-row OpenSOSData request must be split into at least two batches. Each row needs a business name and address; address-free name/state input is not a semantic drop-in for local-business matching. Batch results can include operator legal names, restaurants, and people, and provider billing applies to rows that return at least one person. The contracted provider price is $0.15 per matched location; Deepline applies its standard customer markup on top of that provider amount. Search rows are $0.01 each and direct entity lookup is one $0.01 provider credit; no-result searches and documented not-found lookups are not charged. -
grain.md 1 KB
# Grain Connect a personal or workspace access token from [Grain API settings](https://grain.com/app/settings/integrations?tab=api). Personal tokens see the user's data; workspace tokens can see all workspace data. Deepline does not bill for these tools. Grain account limits still apply. List recordings first, then use recording_id for details or JSON transcripts. Pass the returned cursor to fetch another page. Use include to request optional participants, highlights, AI summaries, action items, or notes. Updates, tags, sharing changes, and hook creation/deletion modify Grain data. Creating a hook also sends a reachability test to hook_url; it must return 2xx. Creating an upload URL requires filename and, for workspace tokens, user_id. Upload the file separately to the returned URL. An upload_status hook receives processing results; the create-upload action itself does not upload a recording. Team sharing, OAuth exchange, binary downloads and text exports are not exposed. See docs.txt for the conflicting team-sharing URL and exact excluded operations. -
hackernews.md 208 B
# Hacker News Guidance Use `hackernews_search` for technical audience research, developer product feedback, launch discussions, and comparison queries. Prefer `sort: "date"` for last-30-days style research. -
harvestapi.md 1.8 KB
# HarvestAPI Choose the action for the LinkedIn resource you need. Use `harvestapi_get_*` for a known profile, company, job, post, group, ad, or engagement target. Use `harvestapi_search_*` for discovery. HarvestAPI interaction actions that send messages or manage connections are intentionally unavailable. For company employee searches, use `harvestapi_search_leads` and pass one or more LinkedIn company URLs or identifiers in `currentCompanies`. HarvestAPI matches this filter by company name even when an ID or URL is provided, so discard results whose `currentPositions.companyId` does not match the intended company ID. Generate a random `sessionId` and reuse it on later pages. For other paginated actions, preserve the returned `paginationToken` when the input supports it. For profile posts, comments, and reactions, `pagination.totalPages: 0` means the total page count is unknown; totals may be inaccurate even when results are present. A returned `paginationToken` guarantees another page exists, so use it rather than totals to decide whether to continue. For profile posts, `postedLimit` may return posts outside the requested duration. Prefer `scrapePostedLimit` when a date cutoff is required, but its enforcement has not been live-verified. HarvestAPI documents no numeric result-limit parameter for this endpoint; request page 1 and truncate `elements` client-side when a fixed count is needed. Use `harvestapi_get_profile` with `main: "true"` when the smaller main-profile response is sufficient. Set `findEmail: "true"` only when email discovery is needed because it costs more. Set `skipSmtp: "true"` only when a non-SMTP email lookup is acceptable. HarvestAPI plan limits are concurrent-request limits, not per-minute quotas. Deepline uses its Business subscription's 40-request concurrency limit and applies shared retry/backoff handling for transient failures. -
heyreach.md 823 B
Use HeyReach for outbound activation after qualification and verification are complete. - Always list campaigns first and resolve the exact campaign target before inserts. - HeyReach public API does not expose campaign creation. Do not attempt to create campaigns via API tools; create campaigns in HeyReach UI first, then reference the resulting `campaign_id`. - Batch writes in small chunks and validate response shape before scaling. - Pull campaign stats after insert operations to confirm downstream effects. ```bash deepline tools execute heyreach_list_campaigns --payload '{}' ``` ```bash deepline tools execute heyreach_add_to_campaign --payload '{"campaign_id":"12345","contacts":[{"linkedin_url":"https://www.linkedin.com/in/example","first_name":"Ada","last_name":"Lovelace","email":"ada@example.com"}]}' ``` -
hubspot.md 2.6 KB
# HubSpot CRM - Agent Guidance ## Quick Reference | Goal | Operation | Notes | | ---------------- | ------------------------ | ---------------------------------------------------------------------------- | | Create a company | `hubspot_create_company` | Use `website_url` when you want HubSpot to infer the domain. | | Create a contact | `hubspot_create_contact` | Prefer `email` for stable identity matching. | | Create a deal | `hubspot_create_deal` | Use `deal_stage` and `deal_probability` only when you know the pipeline. | | Create a ticket | `hubspot_create_ticket` | Use `hubspot_list_ticket_pipelines` first when you do not know stage IDs. | | Create a note | `hubspot_create_note` | `time_stamp` is required. Add associations to place it on a record timeline. | | Create a task | `hubspot_create_task` | `task_type` should usually be `TODO`. | | Update a record | `hubspot_update_*` | Always include `id` and only the fields you want to change. | | Delete a record | `hubspot_delete_*` | Hard delete only when the target should disappear from HubSpot. | | Browse records | `hubspot_list_*` | Use for paging and record inspection. | | Fetch one record | `hubspot_get_object` | Best when you already have the record ID. | | Search records | `hubspot_search_objects` | Best for fuzzy lookups and filters. | ## Practical Notes - HubSpot normalizes most writes to CRM property names such as `firstname`, `lastname`, `hubspot_owner_id`, and `dealstage`. - For object-heavy workflows, prefer `search_objects` over broad listing when you need filters or quick lookup by email/domain. - The `list_objects` and `get_object` helpers work for standard objects and custom objects when you know the object type. - When using notes or tasks, add associations up front so the activity lands on the right record timeline. - For support workflows, prefer the ticket-specific tools over generic object calls: `hubspot_search_tickets`/`hubspot_list_tickets` for dedupe and SLA sweeps, batch ticket tools for backfills and sync repairs, `hubspot_transition_ticket_stage` for status moves, `hubspot_associate_ticket`/`hubspot_remove_ticket_association` for contact/company/GitHub-sync linkage, and `hubspot_add_ticket_note`/`hubspot_pin_ticket_activity` for timeline updates. -
hunter.md 849 B
# Hunter Workflow Guidance - Start with `hunter_discover` and `hunter_email_count` to shape ICP and estimate reachable volume at zero credit cost. - Prefer `hunter_domain_search` when you need multiple contacts from one account; add `department`/`seniority` filters to keep recall high but usable. - Use `hunter_email_finder` for one named person after domain-level search is exhausted or too broad. - Always run `hunter_email_verifier` immediately before outbound send decisions; treat `invalid`, `accept_all`, `webmail`, and `disposable` as non-send states by default. - Use `hunter_people_find` and `hunter_companies_find` for enrichment context, not as your first discovery step. - Use `hunter_combined_find` only when person + company enrichment are both needed in one call and you already have a strong identity seed (email/LinkedIn/domain). -
icypeas.md 1.8 KB
# Icypeas Workflow Guidance - **Always start with counts.** Run `icypeas_count_people` or `icypeas_count_companies` before paid find operations. These are free and let you estimate result volume, refine filters, and avoid wasting credits on overly broad queries. - **Email search is async.** `icypeas_email_search` returns a `SCHEDULED` status immediately. Poll `icypeas_read_results` with the returned `_id` to get the final email. Plan for this delay in workflows. - **Use bulk search for volume.** When processing more than a handful of records, prefer `icypeas_bulk_search` over individual `icypeas_email_search` calls. Bulk supports up to 5,000 rows per batch. Monitor progress via `icypeas_read_bulk_files`. - **Verify before sending.** Always run `icypeas_email_verification` on discovered emails before outbound. Verification is inexpensive relative to search. Treat `NOT_FOUND` and `DEBITED_NOT_FOUND` as non-deliverable. - **LinkedIn scraping is powerful but costs more.** `icypeas_scrape_profile` returns rich contact data including phone numbers and verified emails. `icypeas_scrape_company` is cheaper for company-level data. - **Find-people supports 16 filters.** Use include/exclude arrays for precise targeting: job title, company, location, skills, languages, school, keywords, and more. Start broad with count, then narrow. - **Pagination uses token-based cursors.** For `icypeas_find_people` and `icypeas_find_companies`, pass the `token` from the previous response into the next request's `pagination.token`. Page size max is 200. - **Free operations for planning:** `icypeas_count_people`, `icypeas_count_companies`, `icypeas_read_results`, and `icypeas_read_bulk_files` are free. Use them liberally. - **Check account status.** Use your internal usage dashboard before large operations. -
instantly.md 1.4 KB
Use Instantly for campaign activation and lightweight outbound reporting. - Resolve campaign IDs from `list_campaigns` before any add operation. - For `instantly_create_campaign`, do **not** brute-force timezone guesses. Reuse a known-good timezone from an existing campaign via `instantly_get_campaign` when possible. - If the requested timezone is not directly accepted, map it to the closest supported Instantly value before sending. Example: use `America/Detroit` when the user asks for `America/New_York`. - Treat `UTC` as unsupported by Instantly create-campaign; use a supported UTC-equivalent enum value from existing campaigns instead of sending literal `UTC`. - For `campaign_schedule.schedules[].days`, use numeric day keys (`"0"`..`"6"`). Named weekdays are normalized by Deepline, but values outside sunday..saturday are rejected. - `list_leads` accepts both `campaign` and `campaign_id` (alias). It also supports `list_id`, `in_campaign`, `in_list`, and `search` filters. Omit all filters to list leads globally. - Insert in controlled batches and re-check campaign stats after writes. - Keep activation behind enrichment/verification gates to reduce low-quality sends. ```bash deepline tools execute instantly_list_campaigns --payload '{}' ``` ```bash deepline tools execute instantly_add_to_campaign --payload '{"campaign_id":"abc-123","leads":[{"email":"ada@example.com","first_name":"Ada","last_name":"Lovelace","company_name":"Babbage Ltd"}]}' ``` -
intercom.md 275 B
# Intercom Use only after the workspace connects its own Intercom access token. Prefer read operations for investigation. Mutating conversations, contacts, content, and tickets changes the customer’s Intercom workspace, so perform it only when the caller explicitly asks. -
ipqs.md 1.7 KB
# IPQS — Agent Routing ## Phone validation: use Trestle instead **For phone validation, use `trestle_phone_validation` first, not IPQS.** Trestle returns `activity_score` (0–100) which tells you whether a line is active or disconnected — IPQS phone validation does not. Activity score is the most useful signal for filtering stale numbers before cold outreach. | Scenario | Use | | ------------------------------------------------------ | ------------------------------------ | | Validate a phone after enrichment | `trestle_phone_validation` | | Verify phone belongs to a specific person (name match) | `trestle_real_contact` | | IPQS phone validate | Last resort only — no activity score | ## Email validation: use LeadMagic instead **Do not use `ipqs_email_verify` or `ipqs_batch_email_verify` for outbound email validation.** IPQS email validation returns a boolean `valid` field with no `catch_all` distinction. The Deepline waterfall stack depends on `catch_all` status to decide whether to fall through to the next provider. Use `leadmagic_email_validation` instead — it returns structured `email_status` (`valid` / `catch_all` / `invalid`) and is inexpensive per call. Priority for email validation: `leadmagic_email_validation` → `zerobounce_validate` → IPQS (last resort). ## What IPQS is good for - Fraud scoring: `fraud_score` on email or phone - Disposable/honeypot detection: `disposable`, `honeypot` fields - Spam trap detection on email - DNC status on phone (use `trestle_phone_validation` for line type + activity though) -
kaspr.md 822 B
# Kaspr Use `kaspr_get_linkedin_profile` when you have a LinkedIn profile URL or slug and the person’s full name. Set `dataToGet` to the smallest set the workflow needs: - `workEmail` for professional email addresses - `directEmail` for personal email addresses - `phone` for phone numbers Use `requiredData` only when the workflow should reject a partial match. Every value in `requiredData` must also appear in `dataToGet`. Kaspr does not accept Sales Navigator URLs. Convert them to standard LinkedIn profile URLs first. Deepline bills Kaspr profile lookups by the contact fields requested in `dataToGet`. Phone and direct email are the metered pools; work email is unlimited on the plan and carries only a nominal charge. Request only the fields the workflow needs. Key, rate-limit, and credit reads are free. -
kernel.md 875 B
# Kernel.ai guidance Kernel is asynchronous company-data infrastructure. Start one job, save its `id`, and call only the matching getter until the status is `completed` or `failed`. Do not loop aggressively. Firmographics can take tens of minutes. Use entity resolution when you need a stable Kernel company id. Parentage and firmographics launchers require that id. Use combined when you want identity resolution plus one or both add-ons in one job. Do not send personal data unless Kernel has explicitly approved that data and use case. Do not provide a webhook URL. Deepline has not yet enabled signed Kernel callback ingestion. The paid launchers are currently unavailable. Their nominal Kernel-credit ladder is documented, but the purchased USD exchange rate and Deepline async refund reconciliation are not configured. Existing job status getters remain available. -
leadmagic.md 2.1 KB
Use LeadMagic as a contact-resolution, verification, and intent layer. - Start with cheaper gates: `leadmagic_email_validation`, `leadmagic_company_search`, and `leadmagic_jobs_finder`. - Escalate to premium contact discovery only after the target is worth it: `leadmagic_email_finder`, `leadmagic_mobile_finder`, `leadmagic_profile_search`, `leadmagic_b2b_social_email`, and `leadmagic_email_to_profile`. - For role-based discovery, use `leadmagic_role_finder` for a single decision-maker and `leadmagic_employee_finder` when you need a wider company roster. - `leadmagic_email_validation` is the final outbound validity gate for this layer. Default acceptance rule is `email_status == valid`; treat `catch_all` and `unknown` as unresolved unless the user explicitly accepts risk. - LeadMagic is **conservative on catch-all domains** (many Google Workspace and corporate domains). `email_status: "invalid"` with a populated `mx_record` + `mx_provider` usually means "mailbox not provable," not "does not deliver." Don't auto-discard these rows — fall back to `deepline_native` validation or a second provider (`zerobounce`, `bettercontact`) before giving up on the lead. - If LeadMagic profile or phone quality is noisy on pilot rows, switch to quality-first enrichment (`crustdata_person_enrichment`, `peopledatalabs_enrich_contact`) before scaling. Operational pattern: 1. Run 1-row pilots for identity + email candidates. 2. Validate with `leadmagic_email_validation` on every candidate. 3. Keep fallback chains explicit in your `--with-waterfall` order. 4. Promote only after pilot success and clear assumptions are set. ```bash deepline enrich --input contacts.csv --output contacts.csv.out.csv \ --with-waterfall "email-verify" \ --with '{"alias":"verify_primary","tool":"leadmagic_email_validation","payload":{"email":"{{email_1}}"}}' \ --with '{"alias":"verify_secondary","tool":"leadmagic_email_validation","payload":{"email":"{{email_2}}"}}' \ --end-waterfall ``` Related docs: - [leadmagic_email_validation reference](https://code.deepline.com/tools/leadmagic_email_validation) - [leadmagic_email_finder reference](https://code.deepline.com/tools/leadmagic_email_finder) -
lemlist.md 5.6 KB
Use Lemlist for multi-channel outbound campaigns (email + LinkedIn). Full campaign lifecycle management is available. - Keep activation behind enrichment/verification gates — only push contacts that have been validated. - Resolve campaign IDs from `lemlist_list_campaigns` before any write operation. - Insert contacts in batches of 10–25 and re-check campaign stats after writes. - Always review in Lemlist UI before starting campaigns — use the `web_url` returned by create/update operations. ## Workflow 1. **Create or list campaigns** before adding contacts. 2. **Add sequence steps** (email, LinkedIn invite, LinkedIn DM) to define the outreach flow. 3. **Add contacts** in small batches (10–25), then check stats to verify. 4. **Review in Lemlist UI** before starting — use the `web_url` returned by create/update operations. 5. **Monitor** via activities and inbox threads. ## Quick Reference ### Campaigns ```bash deepline tools execute lemlist_list_campaigns --payload '{}' deepline tools execute lemlist_create_campaign --payload '{"name":"My Campaign"}' deepline tools execute lemlist_pause_campaign --payload '{"campaign_id":"cam_abc123"}' deepline tools execute lemlist_update_campaign --payload '{"campaign_id":"cam_abc123","name":"New Name"}' deepline tools execute lemlist_get_campaign_stats --payload '{"campaign_id":"cam_abc123"}' ``` ### Sequences ```bash deepline tools execute lemlist_get_campaign_sequences --payload '{"campaign_id":"cam_abc123"}' deepline tools execute lemlist_add_sequence_step --payload '{"sequence_id":"seq_abc","type":"linkedinInvite","message":"Hi!","delay":0}' deepline tools execute lemlist_update_sequence_step --payload '{"sequence_id":"seq_abc","step_id":"stp_xyz","type":"linkedinSend","delay":2}' deepline tools execute lemlist_delete_sequence_step --payload '{"sequence_id":"seq_abc","step_id":"stp_xyz"}' ``` ### Leads ```bash deepline tools execute lemlist_add_to_campaign --payload '{"campaign_id":"cam_abc","contacts":[{"email":"ada@example.com","first_name":"Ada","last_name":"Lovelace"}]}' deepline tools execute lemlist_export_campaign_leads --payload '{"campaign_id":"cam_abc","state":"interested"}' deepline tools execute lemlist_pause_lead --payload '{"lead_id":"lea_abc"}' deepline tools execute lemlist_resume_lead --payload '{"lead_id":"lea_abc"}' deepline tools execute lemlist_mark_lead_interested --payload '{"lead_id_or_email":"ada@example.com"}' ``` ### Activities ```bash deepline tools execute lemlist_get_activities --payload '{"campaign_id":"cam_abc","type":"emailsReplied","limit":50}' ``` ### Inbox ```bash deepline tools execute lemlist_list_inbox --payload '{"user_id":"usr_abc"}' deepline tools execute lemlist_get_inbox_thread --payload '{"contact_id":"ctc_abc"}' deepline tools execute lemlist_send_email --payload '{"send_user_id":"usr_abc","send_user_email":"me@co.com","send_user_mailbox_id":"mbx_abc","contact_id":"ctc_abc","lead_id":"lea_abc","subject":"Follow up","message":"<p>Hi!</p>"}' deepline tools execute lemlist_send_linkedin_message --payload '{"send_user_id":"usr_abc","lead_id":"lea_abc","contact_id":"ctc_abc","message":"Thanks for connecting!"}' ``` ### Unsubscribes ```bash deepline tools execute lemlist_list_unsubscribed_variables --payload '{"limit":50}' deepline tools execute lemlist_unsubscribe_variable --payload '{"value":"bounce@example.com"}' deepline tools execute lemlist_resubscribe_variable --payload '{"value":"bounce@example.com"}' deepline tools execute lemlist_export_unsubscribed_variables --payload '{}' deepline tools execute lemlist_get_unsubscribe_by_email --payload '{"email":"bounce@example.com"}' ``` ## Response Shape Contract Deepline wraps all provider payloads in a standard result envelope: `{ data, meta }`. - `lemlist_list_campaigns` → `result.data` is an array of `{ id, name, status }`. - `lemlist_get_campaign_stats` → `result.data` contains `{ sent, opened, clicked, replied, bounced }`. - `lemlist_get_campaign_sequences` → `result.data` is keyed by sequence ID, each with a `steps` array. - `lemlist_export_campaign_leads` → `result.data` is an array of lead objects with `email`, `firstName`, `lastName`, `state`. - `lemlist_add_to_campaign` → `result.data` contains `{ pushed, failed, errors }`. - `lemlist_create_campaign` / `lemlist_update_campaign` → `result.data` includes `web_url` for UI review. ## Key Notes - **Step types:** `email`, `linkedinInvite`, `linkedinSend`, `linkedinVisit`, `manual`, `phone`, `api`, `whatsappMessage`, `conditional`, `sendToAnotherCampaign` - **Delays are in days** for both `add_sequence_step` and `update_sequence_step` (0 = immediate, 2 = 2 days). Do not pass seconds or hours. - **Deep links:** Campaign mutations return `web_url` — always review in Lemlist UI before starting campaigns. - **Lead states for export:** `all`, `contacted`, `interested`, `notInterested`, `emailsBounced`, `paused`, `emailsSent`, `emailsOpened`, `emailsReplied` - **Activity types:** `emailsSent`, `emailsOpened`, `emailsClicked`, `emailsReplied`, `emailsBounced`, `emailsUnsubscribed` ## Gotchas - **Delay unit:** Always days. Passing `172800` thinking "seconds" will result in a `> 1500 days` API error. - **Inbox operations require user/mailbox IDs:** `send_email` needs `send_user_id`, `send_user_email`, and `send_user_mailbox_id`. List inbox first to discover these values. - **Sequence writes:** Prefer adding/updating sequence steps while campaigns are still draft/paused to avoid campaign-state edge cases. - **Lead deduplication:** Validation rejects duplicate emails and duplicate `linkedin_url` values within the same batch. Across batches, Lemlist can reject duplicates (for example 409 conflicts), which surface in `result.data.errors`. -
limadata.md 636 B
Use Limadata for real-time person enrichment, company enrichment, email/phone finding, identity resolution, web search, AI research, and URL extraction. Pricing is settled from Limadata's `x-credits-cost` response header whenever present, except `limadata_find_phone`. Phone lookup settles at the documented 10 Limadata credits only when a phone number is returned. Static credit notes are used for pre-run estimates and fallback settlement for other actions. The current OpenAPI snapshot contains only synchronous POST endpoints. Watch/webhook APIs are not present in this snapshot and are not exposed by this provider registration. -
linkedin_ads_audiences.md 629 B
# LinkedIn Ads Audiences Use these tools for LinkedIn Matched Audiences list-upload workflows. - LinkedIn `LIST_UPLOAD` segments are replacement-oriented. Treat every sync as a refresh, not a true append. - Choose `audience_kind: "contacts"` for hashed-email or people-match lists and `audience_kind: "companies"` for account lists. - Contact lists work best at 10,000+ rows. Company lists work best at 1,000+ companies. - After creating a list-upload segment, LinkedIn recommends a short delay before attaching the uploaded CSV. - Monitor `destination_status` on the returned audience object and expect long processing times. -
luma.md 3.2 KB
# Luma Provider Agent Guidance ## Overview Luma is an event platform API for managing calendars, events, guests, tickets, memberships, and webhooks. All operations require a user-owned Luma API key. ## Authentication - **Required**: User-owned Luma API key - **Header**: `x-luma-api-key` - **Rate Limits**: - Calendar API keys: 200 requests/minute per calendar - Organization API keys: 500 requests/minute per organization - Luma shares each account's quota across GET and POST. Deepline does not apply a provider-wide bucket across customer keys. Honor Luma's `Retry-After` header on 429 responses. ## Priority Operations for GTM Workflows For event workflows, use these operations: 1. **`luma_get_v1_users_get_self`** - Auth check / verify API key 2. **`luma_get_v1_events_get`** - Get event details by event_id 3. **`luma_get_v1_events_guests_list`** - List guests for an event (with filtering by approval_status) 4. **`luma_post_v1_events_guests_update_status`** - Update guest status (approve, waitlist, decline, etc.) ## Common Workflows ### Event Management - Create events: `luma_post_v1_events_create` - Update events: `luma_post_v1_events_update` - List calendar events: `luma_get_v1_calendars_events_list` - Cancel events: `luma_post_v1_events_cancel` ### Guest Management - List guests: `luma_get_v1_events_guests_list` (filter by `approval_status`: approved, session, pending_approval, invited, declined, waitlist) - Get guest details: `luma_get_v1_events_guests_get` - Add guests: `luma_post_v1_events_guests_add` - Update status: `luma_post_v1_events_guests_update_status` with `event_id`, `guest_id`, and `status` (`approved`, `declined`, `pending_approval`, or `waitlist`) - Send invites: `luma_post_v1_events_guests_send_invites` ### Contact Management - List contacts: `luma_get_v1_calendars_contacts_list` - Import contacts: `luma_post_v1_calendars_contacts_import` - Tag contacts: `luma_post_v1_calendars_contact_tags_apply` ## Billing All Luma operations are **no-bill** for Deepline. Access is gated by the caller's Luma API key subscription. Deepline does not purchase or meter Luma provider credits. ## Notes - Event IDs typically start with `evt-` - Use pagination with `pagination_cursor` and `pagination_limit` for list operations - Guest list supports sorting by name, email, created_at, registered_at, checked_in_at - Approval statuses: approved, session, pending_approval, invited, declined, waitlist ## Contract examples The checked-in request fixtures and response examples are synthetic schema examples, not live account data or captured provider responses. Every operation has a request fixture for local validation, including writes. Do not execute these fixtures against an account. Supply real identifiers and review the operation's side effects before calling it. Response examples use the Deepline `data` envelope and preserve Luma field names, such as guest `id`, `user_email`, and `user_name`. Updating a guest's status returns an empty object in `data`. The five response samples pass the checked-in Luma OpenAPI response schemas after removing the outer Deepline `data` envelope. This is schema validation, not live-response verification. See `docs.txt` for the official endpoint references and response evidence limitations. -
lusha.md 2.6 KB
# Lusha — Agent Guidance ## When to use Lusha for B2B email + direct dial enrichment. Strong North American and European coverage with intent signal data. Good for sales prospecting workflows where direct dials matter. Cost-competitive per enriched contact. **Key strength**: Direct dials (not just HQ numbers). Lusha often surfaces mobile and desk direct numbers that other providers miss. ## Provider characteristics - **Input required**: LinkedIn URL (best), email, or first_name+last_name+(company_name or company_domain) - **Geographic coverage**: Global, strongest in North America + Europe - **Cost profile**: billed per person enrich, company enrich, or contact returned from search - **LinkedIn URL requirement**: Must contain "linkedin.com/in/". Sales Navigator URLs not supported. ## Key operations ### lusha_enrich_person Enriches a person by LinkedIn URL (preferred), email, or name+company. Returns emails at `emails[].email` and phones at `phones[].number`. ```json { "linkedin_url": "https://www.linkedin.com/in/johndoe" } ``` ```json { "first_name": "Jane", "last_name": "Smith", "company_domain": "acme.com" } ``` Optional flags: - `reveal_emails: true` — include email addresses in response (default: true) - `reveal_phones: true` — include phone numbers (default: true) - `signals: true` — include intent signal data ### lusha_enrich_company Enriches a company from domain, name, or Lusha company ID. Returns size, revenue, industry, technologies. ```json { "domain": "salesforce.com" } ``` ### lusha_search_contacts Prospecting search with rich filters: department, seniority, company size, industry, job title, location, and tech stack. ```json { "filters": { "seniority": ["director", "vp", "c_suite"], "companySize": ["201-500", "501-1000"], "department": ["sales"] }, "pageSize": 25 } ``` ## Output shape `lusha_enrich_person` returns a flat profile. Email at `emails[0].email` or `email`. Phone at `phones[0].number` or `phone`. `lusha_enrich_company` returns a flat company object with `name`, `domain`, `size`, `industry`, `technologies[]`. `lusha_search_contacts` returns `{ contacts: [...], pagination: { page, pageSize, total, totalPages } }`. ## Anti-patterns - Don't use Sales Navigator or Recruiter LinkedIn URLs — they'll fail - Don't include "http://" or "www." in domain values for company enrichment - Don't pass `company_name` alone without `first_name` + `last_name` for person lookup — name+company is the minimum combo - Don't assume `phone` (top-level) is always populated — check `phones[]` array first for the most complete list -
meta_audiences.md 1.8 KB
# Meta Audiences Use these tools for Meta customer-list custom audiences. - Create one custom audience per segment and then keep syncing member files into the same audience ID. - Use `mode: "replace"` for full snapshots and `mode: "append"` for incremental adds. - Deepline hashes supported identifiers locally before upload. - Watch `operation_status`, `delivery_status`, the `approximate_count_*_bound` pair, and invalid-entry counts when evaluating match health. - Meta reports audience size as a range. Read `approximate_count_lower_bound` and `approximate_count_upper_bound`; `approximate_count` is a midpoint Deepline derives for backward compatibility, not a figure Meta returns. - Sizes stay null until Meta finishes processing an upload, and Meta withholds them entirely for audiences below its minimum size threshold. A null count shortly after a sync is normal, not a failed upload. - `delivery_status` code 411 means a low rate of matched people, which is the signal that a list matched poorly. - Meta locks an audience while a users upload ingests: `operation_status` code 414 means a replace is still processing, and further writes fail with `META_AUDIENCE_UPDATE_IN_PROGRESS` until it settles. Poll `meta_audiences_get_audience_status` until `operation_status` code returns 200, then retry the full sync in one call. Do not resume a partial upload with `mode: "append"`; re-send the complete list. - Send the whole member list in one `sync_audience_members` call. Chunking into separate calls collides with the ingest lock and strands a partial audience. - `audience_id` must be the numeric id Meta returns. Read it from `meta_audiences_create_audience` at `data.audience.id`; a literal "undefined" id means the caller read the wrong response field, and the tool now rejects it before compiling the payload. -
nooks.md 1013 B
# Nooks Use Nooks to inspect a connected customer's sales-engagement workspace. - The internal test endpoint uses hidden `nooks_get_me` to verify the credential and identify the connected workspace. - Use list actions to find IDs, then fetch a specific sequence, prospect, account, task, call, mailbox, user, sequence state, step, disposition, or email template. - Keep pagination bounded. Nooks supports cursor pagination with `page[size]` up to 100 and `page[after]` or `page[before]`. - Use at most three comma-separated top-level `include` values. Nested includes are not supported. - All mutations are unavailable until Deepline can validate them in an internal test workspace. `nooks_create_sequence_state` has the additional risk that it can trigger immediate enrichment and consume workspace entitlements. Email reads remain unavailable until complete provider-owned response evidence is available. - Do not use the deferred `call.logged` webhook as a polling or monitor substitute. -
openmart.md 1.6 KB
# Openmart Guidance Use Openmart for local-business and SMB workflows where store-level location data, brand records, shared company emails, known-person enrichment, employee discovery, or technology detection matter. - Prefer `openmart_search_brands` when the workflow needs one row per company/brand. - Prefer `openmart_search_businesses` when the workflow needs physical store records, ratings, categories, addresses, or location filters. - Use `openmart_enrich_company` when the input is a website or social profile and the goal is to resolve matching Openmart records. - Async launchers default to synchronous execution: they submit the batch, wait for completion, and return completed task results. Set `wait_for_completion: false` when the workflow only needs a `batch_id`. - For manually managed async jobs, poll `openmart_get_batch_status`, list completed IDs with `openmart_get_batch_task_ids`, then fetch each result with `openmart_get_task_result`. - Treat `null` arrays in brand responses as empty arrays. - Openmart docs require `country` on each `/api/v2/brands/search` location entry. - The configured provider token should be rotated if it has been pasted into chat, logs, or other plaintext surfaces. Pricing note: search, enrichment, and technographics are billed per returned record, and phone lookups are the most expensive result type — bound `limit` when exploring. Openmart bills successful results only; failed calls and no-result calls are not billed. Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. -
opensosdata.md 4 KB
# OpenSOSData Integration Guide US Secretary of State business entity lookup across currently active OpenSOSData jurisdictions. ## When to use Use `opensosdata_business_lookup` to find the registered officer name for a business entity. This is the bridge between "I have a restaurant name" and "I have an owner name to skip-trace." Typical flow: 1. `opensosdata_business_lookup` → get officer name from SOS 2. `enformion_contact_enrich` → get mobile phone from officer name + city/state ## State-by-state officer data coverage | Coverage | States | Notes | | --------------------------------- | ------------------------------------------------------ | --------------------------------------------- | | **Full** (name + address + title) | FL, TX (franchise tax), CO, PA, MN, NY, WI, KY, SC, RI | FL is best — full officer list with addresses | | **Name only** | IL, IN, TN, CT, MA, GA, NV, ND, AL, AR, IA, MO | | | **Entity found, no officers** | OH, CA, MI, WA, NJ | SOS shields member names — use other methods | ## Critical gotchas ### 1. FL officer name format — ALWAYS reverse FL SOS returns `"LASTNAME, FIRSTNAME MIDDLE"`. Use `reverseOfficerName()` from `opensosdata-shared.ts`: - `"SMITH, JOHN EDWARD"` → `"John Smith"` - `"DE LA CRUZ, MARIA"` → `"Maria De La Cruz"` Other states that use this format: LA, SC, sometimes GA. ### 2. Async states (CA, MA, NV, OR, WA) These states return HTTP 202 with a `jobId`. The action **automatically polls** every 3 seconds until complete (up to 90s). No special handling needed by the caller. ### Native bulk jobs `opensosdata_bulk_lookup` submits up to 1,000 entities in one provider request. It waits at most 30 seconds, then returns the provider-issued `job_id` for `opensosdata_get_bulk_result`. Calls to `opensosdata_business_lookup` inside a dataset map compile into native jobs of up to 256 entities. Keep using the scalar tool for row-wise play code; call the bulk operation directly only when you already have an entity array. After OpenSOSData accepts a bulk job, Deepline never retries the POST. Polling timeouts and transport failures return that `job_id`. The result cache never stores or resumes the job; callers can make the explicit status/result call. Billing separately tracks accepted provider work until terminal usage is known. ### 3. Skip registered agent services Filter officer names containing: "Corporation Service", "CT Corporation", "Incorp", "Northwest Registered", "Statutory Agent". These are professional RA services, not people. Use `isRegisteredAgentService()` from `opensosdata-shared.ts`. ### 4. Balance monitoring Each lookup costs Deepline credits. Check remaining balance before large batch runs. Check balance: `GET /v1/account/balance` → `lookupsRemaining`. Topup URL: https://app.opensosdata.com#billing ## Response structure Synchronous (most states): ```json { "success": true, "data": { "entityName": "NOBLE BEAST BREWING LLC", "entityType": "LLC", "entityId": "2441200", "status": "Active", "formationDate": "10/28/2015", "registeredAgentName": "", "officers": [], "sosUrl": "https://businesssearch.ohiosos.gov/...", "scrapedAt": "2026-05-31T..." } } ``` FL with full officers: ```json { "success": true, "data": { "entityName": "CASTAWAYS RIVER TIKI BAR LLC", "officers": [ { "name": "SWANSON, KIRK ALAN", "title": "MGR", "address": "..." } ] } } ``` → Reverse: `reverseOfficerName("SWANSON, KIRK ALAN")` = `"Kirk Swanson"` Async (CA/MA/NV/OR/WA) — handled automatically, caller receives final result. Not found (no charge): ```json { "success": false, "error": "Entity not found", "cost": 0 } ``` ## Credentials Single API key passed as `x-api-key` header. Set via Deepline dashboard → Integrations → OpenSOSData, or `OPENSOSDATA_API_KEY` env var. API endpoint: `https://api.opensosdata.com/v1/lookup` -
openwebninja.md 315 B
Use `openwebninja_jsearch_*` for Google for Jobs data, `openwebninja_glassdoor_*` for employer/review/salary data, and `openwebninja_localbusiness_*` for Google Maps business data. Prefer the narrowest endpoint that matches the task, and pass official ids from search endpoints into detail endpoints when possible. -
outreach.md 2.6 KB
# Outreach agent guidance Connect Outreach with OAuth before executing a tool. Do not ask for or accept a static Outreach API token. Use `outreach_list_accounts` and `outreach_list_prospects` before creating records. This prevents duplicates and gives you the numeric relationship IDs required by JSON:API writes. Preserve JSON:API request bodies: ```json { "data": { "type": "prospect", "attributes": {}, "relationships": {} } } ``` For collection reads, `page_size` returns one page but Deepline does not currently surface the next cursor, so `page_after` cannot continue the walk. To read beyond one page, use `page_limit` with an increasing `page_offset`. Set `count: true` when you need the filtered total without fetching every row. Use `fields` to reduce large records and `include` only when the related records are needed. Date filters accept either `filters: { createdAt: "2026-07-18..2026-08-01" }` or, with `newFilterSyntax: true`, `filters: { createdAt: { gte, lte } }`. Do not combine `newFilterSyntax: true` with a `..` string range; that pairing fails with `filterParameter.invalidDatetimeFormat`. To add a prospect to a sequence, first resolve the prospect, sequence, and mailbox IDs. Then call `outreach_create_sequence_state`. Use the dedicated finish, pause, and resume actions for state transitions. Outreach write and action calls change the connected workspace. Confirm the target IDs and intended mutation before execution. For reporting, read `outreach_list_mailings` for send and engagement activity, `outreach_list_sequences` plus `outreach_list_sequence_steps` for campaign structure, `outreach_list_opportunities` for pipeline, and `outreach_list_tasks` or `outreach_list_calls` for rep activity. Records reference lookup tables by ID, so resolve names through `outreach_list_opportunity_stages`, `outreach_list_stages`, `outreach_list_personas`, and `outreach_list_teams` rather than guessing what an ID means. Webhooks are available through `outreach_list_webhooks`, `outreach_get_webhook`, `outreach_create_webhook`, and `outreach_update_webhook`. A webhook delivers Outreach events to a URL you control, so confirm the target URL, resource, and action before creating one, and prefer updating an existing subscription over adding a duplicate. Bulk and batch operations, imports, and record deletion for accounts, prospects, sequence states, and webhooks are not exposed, and there is no generic passthrough action for them. Deepline maps 58 of the 253 documented Outreach operations. If no typed action covers what you need, say so rather than improvising: `generic_http_request` cannot read the connected OAuth credential, so it is not a substitute. -
parallel.md 3.4 KB
Use Parallel for managed research/extraction runs without custom orchestration. - Use `parallel_run_task`, `parallel_search`, and `parallel_extract` for agent-friendly workflows. - Use the paid REST actions `parallel_search` and `parallel_extract` for every Play, enrich, map, dataset, batch, repeated, unattended, or production workload. - The free anonymous `parallel_search_mcp` and `parallel_fetch_mcp` actions are exact-name tools for small, direct, one-off exploration only. They are intentionally omitted from general tool discovery. - Never put a free MCP action in a Play or scale loop. Use `parallel_search` or `parallel_extract` instead. - Prefer `parallel_search` first for attendee/discovery workflows, then `parallel_extract` for targeted pages. - `parallel_search_mcp` is only for an explicitly requested lightweight lookup where zero provider spend matters more than reliability or REST-side controls. - `parallel_fetch_mcp` is only for directly reading a small set of URLs during that same exploratory session. - Use `parallel_run_task` when you need synthesized, schema-shaped outputs from multiple sources. - Call `parallel_run_task` first. If it finishes quickly, use that result. - If `parallel_run_task` returns pending or times out, keep the `run_id` and use `parallel_get_task_run_result` later to fetch the final output. - Ignore `parallel_get_task_run` unless you specifically need run metadata like status timestamps or processor info. - Keep monitor/stream endpoints out of default flows unless a user explicitly needs them. - Pilot on a small objective first, then widen `max_results` and scope. - For a direct exploratory MCP call, pass a stable `session_id` across related calls when possible to reduce anonymous-tier throttling. A session id does not make MCP suitable for scale. ```bash deepline tools execute parallel_search --payload '{"mode":"agentic","objective":"Find recent hiring and launch signals for OpenAI","max_results":5,"excerpts":{"max_chars_per_result":1200,"max_chars_total":10000}}' ``` ```bash deepline tools execute parallel_search_mcp --payload '{"objective":"Find recent OpenAI product announcements","search_queries":["OpenAI recent announcements","site:openai.com/news OpenAI product"],"session_id":"demo-session"}' ``` ```bash deepline tools execute parallel_extract --payload '{"urls":["https://openai.com/research/index/release/"],"objective":"Extract key product launch signal, release summary, and source evidence","full_content":true}' ``` ```bash deepline tools execute parallel_fetch_mcp --payload '{"urls":["https://openai.com/news"],"objective":"Extract the latest product announcement headlines","search_queries":["OpenAI latest product announcements"],"session_id":"demo-session"}' ``` ```bash deepline tools execute parallel_run_task --payload '{"processor":"lite-fast","input":"Summarize key GTM signals for OpenAI from recent public web sources in 3 bullets."}' ``` ```bash deepline tools execute parallel_get_task_run_result --payload '{"run_id":"trun_123"}' ``` ```bash deepline tools execute parallel_search --payload '{"objective":"Find AI companies that raised Series A funding in 2024 with source links","max_results":10}' deepline tools execute parallel_extract --payload '{"urls":["https://techcrunch.com/2024/12/20/heres-the-full-list-of-49-us-ai-startups-that-have-raised-100m-or-more-in-2024/"],"objective":"Extract company name, funding round, amount, date, and source evidence","excerpts":true}' ``` -
peopledatalabs.md 3.6 KB
Use People Data Labs when you need explicit, auditable structured filters. - Normalize noisy input first with clean helpers before running expensive search/enrich operations. - Use autocomplete and narrow incrementally to avoid over-constraining initial queries. - Treat Person Search `size` as a spend cap: every returned profile is billed. Start with `size: 1`, inspect `total` and field coverage, then request only the number of profiles the user can use. - For surgical gap-fill, prefer `size: 3-5`. Do not default to pages of 30, 40, or 100 after earlier providers have already returned candidates; post-response deduplication cannot recover the PDL spend. - Put must-have fields into the search itself, for example `work_email IS NOT NULL`, `personal_emails IS NOT NULL`, `mobile_phone IS NOT NULL`, or an Elasticsearch `exists` clause. Use the matching `dataset` slice where appropriate. - For `peopledatalabs_person_search` / `peopledatalabs_company_search` SQL: use `SELECT *` only and DO NOT include a `LIMIT` clause — PDL rejects any SQL with `LIMIT` as HTTP 400. Pass the `size` input parameter (1–100) to control how many records come back. - `required` and `min_likelihood` are Person Enrichment controls, not Person Search inputs. Use `peopledatalabs_enrich_contact` when you already know the approximate person and need one strict match. - Enrichment also accepts `data_include`: comma-separated fields include data and a leading `-` excludes data. Suppressing the data payload requires the literal two-character value `""` (a quote pair) — passing a bare empty value is silently ignored and returns the full record. Projection does not lower credits either way; use `required` and `min_likelihood` to control which matches are billable. - Bulk person and company enrichment preserve these controls. Person bulk details may override shared controls per request. Company bulk controls apply to every domain in the batch. - De-duplicate and normalize the input list before any bulk enrichment. `peopledatalabs_bulk_people_enrichment` and `peopledatalabs_bulk_organization_enrichment` bill per matched row, and PDL does not collapse repeats: two rows for the same person cost 2 credits, and `stripe.com`, `www.stripe.com`, and `name: stripe` were each billed even though all three resolved to the same PDL company id. Exact-string de-duplication is not enough — strip URL schemes, `www.`, and trailing slashes, and reconcile name-vs-domain rows to one identifier per entity first. Deepline's batch item keys do not collapse these variants for you, and rows are matched to responses by position, so the batch cannot de-duplicate them safely on your behalf. - For personal-email-only use cases, require `personal_emails` before billing by passing PDL's `required=personal_emails` parameter. The default Person Enrichment API bills per matched person profile, even if no personal email is present. - PDL documents `x-call-credits-spent` as the per-call charge response header. Deepline parses that header into `meta.creditsSpent` and prefers it for billing before any fallback estimate. - In changed-company email recovery, treat PDL as the fallback after LeadMagic and Crust. - If earlier, cheaper steps already returned a usable email, skip PDL for that row. ```bash deepline tools execute peopledatalabs_company_clean --payload '{"name":"Open AI Inc"}' ``` ```bash deepline tools execute peopledatalabs_person_search --payload '{"query":{"bool":{"must":[{"term":{"location_country":"united states"}},{"term":{"job_title_role":"marketing"}}]}},"size":5}' ``` ```bash deepline tools execute peopledatalabs_autocomplete --payload '{"field":"title","text":"growth"}' ``` -
pipedrive.md 582 B
# Pipedrive Use Pipedrive only with the workspace's own API token. Start with read operations such as listing users, pipelines, stages, or searching records to discover identifiers before writes. Create, update, archive, convert, and delete operations modify customer CRM data. Inspect the generated tool description and required identifiers before executing them. List endpoints use cursor pagination; pass the returned cursor to continue. Pipedrive also applies an account token budget whose per-operation cost is documented as `x-token-cost` in the upstream API specification. -
podscan.md 2.2 KB
# Podscan Podscan indexes and transcribes the podcast ecosystem and exposes full-text search across every transcript. ## When to use - "Find every podcast where {topic / brand / competitor} was discussed." - "Who talked about {subject} on a podcast?" — episode transcripts carry structured host/guest metadata (name, company, occupation, industry). - Social-listening / brand-monitoring over spoken audio, not just text. - Sourcing warm outbound targets: podcast guests who discussed your category are high-intent, self-identified buyers. ## Operations - `podscan_episodes_search` — full-text transcript search. Returns matching episodes with the matched `_search_highlight` snippet, the parent `podcast`, `metadata.hosts[]` / `metadata.guests[]` (name + company + occupation), and AI-extracted `topics[]` with sentiment. Use quoted phrases for precision, e.g. `"customer interviews"`. Full transcripts are omitted by default (they are large); pass `include_transcript: true` only when you need them. - `podscan_podcasts_search` — discover shows by title/description. ## Tips - Quote multi-word phrases to avoid loose matches. - `pagination.total` gives the corpus-wide mention count for a query — useful for sizing before pulling pages. - `language` filters by ISO 639-1 code (e.g. `en`) and is honored by the API. Date and category filtering are not currently exposed because Podscan's documented search surface does not reliably support them. - Turn guests into contactable leads by piping `guest_name` + `guest_company` into LinkedIn resolution and an email waterfall. ## Auth & billing Deepline supplies the Podscan credential (`PODSCAN_API_KEY`). Billing applies per successful search — a well-formed response with at least one result. Zero-match searches are free; a malformed provider response is rejected rather than billed as an empty result. Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. ## Rate limits Podscan rate-limits aggressively (roughly 10 req/min on trial plans, higher on paid). The connector applies a conservative shared limit and surfaces upstream 429s; prefer `deepline enrich`, which paces requests automatically. -
postgres.md 553 B
Use `postgres_run_query` when the task requires SQL against the user's connected Postgres database. It accepts one unrestricted statement and always uses the saved integration credentials; never ask for or place connection details in a tool call. Prefer parameterized queries with `binds`. Mutations and DDL execute directly without a `write` opt-in flag, so perform side effects only when the user requests them. The database user's permissions are the access-control boundary. Never expose the connection string, password, or database-provider spend. -
predictleads.md 622 B
# PredictLeads Guidance Use PredictLeads for company-level signals: hiring, technology detections, news events, financing events, connections, products, GitHub repositories, and similar companies. Prefer direct company endpoints when you already have a domain. They cost one API credit per request and can return up to 1,000 records on list endpoints. Use discovery endpoints only when you need broad search, because they bill per returned result. Avoid follow/unfollow workflows for now. PredictLeads followed-company APIs are designed for webhook delivery and recurring monthly billing, not one-shot agent execution. -
prospeo.md 1.4 KB
# Prospeo Workflow Guidance - Use `prospeo_search_person` or `prospeo_search_company` to build list-level candidates, then refine with `prospeo_enrich_person` and `prospeo_enrich_company` for details. - Use `prospeo_search_person` for prospecting -- it supports stable filters (job title with boolean operators, department, seniority, industry, headcount, technology, location). Search is billed per page of 25 results. - Do not use Prospeo for job-change detection or job-change filtered searches. The live Prospeo job-change filter has schema drift; use FullEnrich for job-change workflows. - Use `prospeo_search_company` to build account lists by firmographic criteria before drilling into individual contacts. - Use `prospeo_enrich_person` for full profile enrichment when you need more than just an email (title, company, location). **Mobile phone reveal (`enrich_mobile: true` or `only_verified_mobile: true`) can cost up to 10× a standard enrichment.** The tool's payload estimate includes that 10× maximum so Play spend guards can admit or reject the request before provider execution. Only enable mobile reveal when phone outreach is explicitly requested. - Use `prospeo_enrich_company` for firmographic enrichment (industry, headcount, technologies, description) from a website, company name, or LinkedIn company URL. Recommended workflow: `prospeo_search_person` or `prospeo_search_company` to build lists, then `prospeo_enrich_person` for individual contacts. -
quickenrich.md 482 B
Use QuickEnrich lookup endpoints before contact or company finder searches when a filter requires an exact provider value. Use `quickenrich_contact_search` to find masked candidates for free. Reveal only selected rows with `quickenrich_contact_reveal`. Check each tool's Deepline pricing before running billable email, phone, reverse-email, dataset, reveal, or company searches. A customer-owned API key takes precedence over Deepline's managed key and is not billed by Deepline. -
rapidapi.md 2.5 KB
Use RapidAPI when a registered marketplace listing covers the data you need. RapidAPI is a marketplace like Apify: one Deepline-managed key reaches every listing Deepline is subscribed to, but each listing bills against its own plan, so only listings registered in Deepline can be called. - Prefer the typed listing tools first: `rapidapi_google_web_search` for Google organic results and `rapidapi_google_image_search` for Google Images. They validate inputs, unwrap the listing's response envelope, and publish an exact per-page price. - Use `rapidapi_list_listings` (free) when you do not know which listings or endpoints exist. It returns each listing's host, documented endpoints, required inputs, and the typed tool for each endpoint. - Use `rapidapi_request` only for an endpoint on a registered listing that has no typed tool yet. Pass the listing `host` (or slug), the `path`, and `query` or `body`. Hosts that are not registered are rejected before any call because Deepline cannot price them; ask the Deepline team to register a listing you need. - Every call is billed per RapidAPI request. Paginated endpoints bill one request per page: start with `pages: 1` and raise it only when you need more than about 20 results, because a larger `pages` value re-bills the earlier pages. - An empty result set from a search listing is a completed search, not an error, and it is still billed. Do not retry it automatically. - `billed_requests` in every result is the count the listing reported in its `X-RapidAPI-Billing` header. Deepline settles the call from that count; when the header is absent it charges the requested count. - Upstream 401 means the RapidAPI key is not subscribed to that listing. 404 and 410 come back as `ok: false` with the upstream body so you can inspect them; 429 and 5xx stay loud so the shared retry policy applies. - Deepline paces calls to the smallest paid plan's per-second limit across the provider (5 requests per second today); bursts wait briefly for a slot instead of receiving a synthetic 429. ## Examples ```bash # See which listings and endpoints are registered deepline tools execute rapidapi_list_listings --payload '{}' ``` ```bash # Typed Google web search, one page (about 20 results) deepline tools execute rapidapi_google_web_search --payload '{"query":"best laptop 2026","pages":1,"country":"us"}' ``` ```bash # Generic call to a registered listing endpoint deepline tools execute rapidapi_request --payload '{"host":"real-time-google-search-api","path":"/api/v1/google-search/image","query":{"q":"best laptop 2026"}}' ``` -
redshift.md 763 B
Use Redshift when the task requires querying an organization's warehouse data. Prefer `redshift_run_semantic_query` when a saved semantic layer exists or the user asks for business metrics, dimensions, filters, funnels, or model-defined entities. The semantic query tool renders the stored Redshift semantic layer into SQL and returns both rows and the rendered SQL for inspection. Use `redshift_run_query` only when the user provides raw SQL, asks for direct SQL, or the semantic layer does not cover the requested analysis. Leave `write` unset for read-only `SELECT` or `WITH` SQL. Set `write: true` only when the user intentionally wants a single statement that may modify or overwrite customer data. Never expose Redshift warehouse spend as Deepline spend. -
salesforce.md 1.2 KB
Use `salesforce_fetch_fields` before writing custom objects or unknown standard objects so you can confirm exact field API names and validation rules. Use `salesforce_list_contacts`, `salesforce_list_leads`, and `salesforce_list_accounts` for incremental CRM reads. They accept `modified_after` for recent changes and `next_records_url` for pagination handoff. Use the object-specific create, update, and delete tools for Accounts, Contacts, Leads, and Opportunities instead of building raw Salesforce payloads yourself. The integration already maps Deepline-friendly field names to Salesforce API names. For custom field write-back on Accounts, Contacts, Leads, and Opportunities, pass `fields` with official Salesforce API names, for example `{ "id": "001...", "fields": { "Deepline_Score__c": 87, "Deepline_Email_Gate__c": true } }`. Values may be strings, numbers, booleans, or `null` to clear a field. If a friendly field and `fields` both target the same Salesforce API name, the explicit `fields` value wins. After a write, use `salesforce_get_record` with the object API name, record ID, and exact scalar field API names to verify custom field values, for example `{ "object": "Account", "id": "001...", "fields": ["Name", "Deepline_Score__c"] }`. -
salesforge.md 274 B
# Salesforge Use only after the workspace connects its own Salesforge API key. Reads are safe for inspection. Creating, launching, updating, or deleting sequences, contacts, mailboxes, and webhooks changes the customer’s outbound system and requires an explicit request. -
salesloft.md 1 KB
# Salesloft agent guidance Salesloft tools use the workspace's own Salesloft API key. Deepline does not charge for provider usage. The connected key must include every scope required by the operations an agent calls. Prefer read operations before mutations. Resolve stable Salesloft IDs with list or fetch operations, then pass those IDs to create, update, delete, cadence-membership, activity, or workflow operations. Read pagination metadata and continue paging when a complete result set is required. Creation, update, deletion, sending, redaction, import, bulk-job, webhook, and signal operations change customer data or start provider work. Confirm the intended records and payload before calling them. The operation description and generated schema are the source of truth for required identifiers and fields. Operations documented only as `multipart/form-data` are registered but disabled because the shared V2 executor cannot yet encode multipart requests safely. Do not substitute JSON or work around the disabled status. -
scrapecreators.md 2.1 KB
# ScrapeCreators Guidance Use ScrapeCreators when the research plan needs public social evidence that generic web search cannot capture well: Reddit comments, TikTok videos/comments, Instagram Reels, and YouTube transcripts/search. For local-business or restaurant contact recovery, ScrapeCreators is an optional candidate route when the row already has a Facebook or Instagram URL/handle, when Maps/website data is thin, or when a pilot suggests public profile contact fields are where the email lives. Do not make it a required step for every SMB workflow; test it on a tiny sample and keep Maps + website extraction as the default first pass. Good candidate tools: - `scrapecreators_facebook_profile`: public Facebook page/profile About data, including business email/phone/address/website when available. - `scrapecreators_facebook_profile_posts`: recent post text, useful when the About block lacks an email but posts mention catering, reservations, bookings, or contact details. - `scrapecreators_instagram_profile`: public Instagram profile bio/contact fields, including business public email/contact button/website when available. - `scrapecreators_instagram_user_posts`: post/caption text, useful when the bio links to booking, menu, or contact pages. Tool discovery nuance: profile/contact endpoints are research candidates, but if an installed SDK still categorizes them outside `research`, run an unfiltered ScrapeCreators search as a fallback: ```bash deepline tools search "facebook profile email scrapecreators" --json deepline tools search "instagram profile bio email scrapecreators" --json deepline tools search scrapecreators --json ``` When using social profile data for SMB contact email recovery, require identity evidence before accepting the result: match at least two of business name, address, phone, website/menu/booking link, or Google Maps profile. Return the source platform, profile URL, extracted email/contact field, timestamp, and identity evidence columns so the result can be audited. Pricing note: Deepline credit pricing for these endpoints is generated from the provider pricing metadata and rendered on the public provider pages. -
searchbug.md 637 B
# Searchbug Workflow Guidance Use `searchbug_phone_validation` for US and Canadian phone numbers when you need connection status, line type, carrier, porting, or DNC/TCPA fields. It is a validation/compliance check, not a person-to-phone identity match. The connector strips punctuation and a leading US/Canada country code before calling Searchbug's `api_lnp3` phone validator. The complete provider response is returned so newly added provider fields remain available without a Deepline release. Use Trestle when you need activity scoring or identity matching, and use IPQS for fraud-risk screening after ordinary phone validation. -
sec_edgar.md 1.4 KB
# SEC EDGAR guidance Use SEC EDGAR for authoritative public-company filings and facts reported in those filings. 1. Resolve an exact ticker with `sec_edgar_resolve_company` when the CIK is unknown. 2. Use `sec_edgar_list_filings` to find the accession and primary document. Filter to forms such as `10-K`, `10-Q`, and earnings-related `8-K` filings when appropriate. 3. Use `sec_edgar_get_filing_index` before document retrieval so the exact primary document or exhibit name is known. 4. Use `sec_edgar_get_filing_document` with a bounded `max_chars`. Follow its `source_url` when the result is truncated. 5. Use `sec_edgar_get_company_concept` for one exact XBRL taxonomy tag. Preserve the returned unit, period, form, accession, and filing URL when citing a value. Structured results are available at `toolResponse.raw`; company-concept facts are under `toolResponse.raw.units[].facts[]`. Type-check each `value`, and select a fact by unit, form, dates or SEC frame, and accession rather than assuming the first fact is the desired standalone quarter. Use filing documents for narrative text, not as the default source for a fact already present in XBRL JSON. SEC facts are disclosures, not a canonical normalized financial statement. Company-specific extension tags, fiscal calendars, duplicate contexts, and restatements require interpretation. SEC EDGAR does not provide stock prices, analyst estimates, earnings calendars, or transcript feeds. -
sentrion.md 470 B
# Sentrion Guidance Use Sentrion for hiring-signal research and job market intelligence. - Use `sentrion_company_jobs_search` when the company LinkedIn URL is known. - Use `sentrion_jobs_search` for broad market searches across companies. - Use historical actions only when the user or workspace has Sentrion historical access. - Paginate with `search_after` when `result.data.search_after` is non-null. - Prefer narrow filters and an explicit `limit` when exploring. -
serper.md 1011 B
# Serper Agent Guidance Use Serper when you need live Google results fast and broad recall matters more than source-specific extraction. Prefer `serper_google_search` for: - broad web research - newsy or changing facts - finding a company, person, or topic before deeper enrichment - collecting candidate URLs to hand off to extraction tools Prefer `serper_google_maps_search` for: - local business discovery - location-aware company lookups - phone, address, rating, website, or CID retrieval Practical guidance: - Start with Serper before heavier browser or extraction workflows when you do not yet know the right destination URL. - Treat Serper as a discovery and recall layer, then pass strong hits into structured tools like Firecrawl, Apify, or provider-specific enrichments. - Use Maps search when the user cares about storefronts, service areas, offices, or other local entities. - Expect live-search variability. If a result is important, validate it with the returned URL or a follow-up fetch. -
smartlead.md 10 KB
Use Smartlead for outbound email campaign management. Full lifecycle from campaign creation through lead push, scheduling, sequencing, and monitoring. - Keep activation behind enrichment/verification gates -- only push contacts that have been validated. - Resolve campaign IDs from `smartlead_list_campaigns` before any write operation. - Push leads in batches of up to 400 and re-check campaign stats after writes. - Keep Smartlead traffic at or below 60 requests per 60 seconds per API key. Large pools need queueing or smaller concurrency. - Always configure sequences and schedule before starting a campaign. - Include `SMARTLEAD_API_KEY` as fallback env credential when not using org-linked auth. - Keep payloads provider-native. There is no shared outbound standard contract for Smartlead. ## Workflow 1. **Create or list campaigns** to get a stable campaign ID. 2. **Add email accounts** to the campaign using `smartlead_add_campaign_email_account`. 3. **Configure sequences** with `smartlead_save_campaign_sequences` (email steps, delays, variants). 4. **Set the schedule** with `smartlead_update_campaign_schedule` (timezone, days, hours, send rate). 5. **Configure settings** with `smartlead_update_campaign_settings` (tracking, stop conditions). 6. **Push leads** in batches (max 400) using `smartlead_push_to_campaign`. 7. **Start the campaign** with `smartlead_update_campaign_status` (status: `START`). 8. **Monitor** via `smartlead_get_campaign_stats` and `smartlead_get_campaign_analytics`. ## Quick Reference ### Campaigns ```bash deepline tools execute smartlead_list_campaigns --payload '{}' deepline tools execute smartlead_create_campaign --payload '{"name":"Insurance Brokerage - Book Assessment"}' deepline tools execute smartlead_get_campaign --payload '{"campaign_id":12345678}' # smartlead_clone_campaign is disabled pending live contract verification. deepline tools execute smartlead_update_campaign_status --payload '{"campaign_id":12345678,"status":"START"}' deepline tools execute smartlead_delete_campaign --payload '{"campaign_id":12345678}' ``` ### Leads ```bash deepline tools execute smartlead_push_to_campaign --payload '{"campaign_id":"12345678","lead_list":[{"email":"jane@example.com","first_name":"Jane","last_name":"Lovelace","company_name":"Acme Corp","custom_fields":{"tag":"demo"}}]}' deepline tools execute smartlead_list_campaign_leads --payload '{"campaign_id":12345678,"offset":0,"limit":100}' deepline tools execute smartlead_fetch_lead_by_email --payload '{"email":"jane@example.com"}' deepline tools execute smartlead_update_lead_by_campaign --payload '{"campaign_id":12345678,"lead_id":1,"email":"jane@example.com","first_name":"Updated"}' deepline tools execute smartlead_pause_lead_by_campaign --payload '{"campaign_id":12345678,"lead_id":1}' deepline tools execute smartlead_resume_lead_by_campaign --payload '{"campaign_id":12345678,"lead_id":1}' deepline tools execute smartlead_unsubscribe_lead --payload '{"lead_id":1}' deepline tools execute smartlead_export_campaign_leads --payload '{"campaign_id":12345678}' ``` ### Sequences ```bash deepline tools execute smartlead_fetch_campaign_sequences --payload '{"campaign_id":12345678}' deepline tools execute smartlead_save_campaign_sequences --payload '{"campaign_id":12345678,"sequences":[{"seq_number":1,"seq_delay_details":{"delay_in_days":0},"seq_variants":[{"email_body":"<p>Hello Ada</p>","variant_label":"A","subject":"Quick question"}]},{"seq_number":2,"seq_delay_details":{"delay_in_days":3},"seq_variants":[{"email_body":"<p>Following up</p>","variant_label":"A","subject":"Re: Quick question"}]}]}' ``` ### Schedule and Settings ```bash deepline tools execute smartlead_update_campaign_schedule --payload '{"campaign_id":12345678,"timezone":"America/New_York","days_of_the_week":[1,2,3,4,5],"start_hour":"09:00","end_hour":"17:00","min_time_btw_emails":5,"max_new_leads_per_day":20}' deepline tools execute smartlead_update_campaign_settings --payload '{"campaign_id":12345678,"track_settings":["DONT_TRACK_EMAIL_OPEN"],"stop_lead_settings":"REPLY_TO_AN_EMAIL"}' ``` ### Analytics ```bash deepline tools execute smartlead_get_campaign_stats --payload '{"campaign_id":12345678}' deepline tools execute smartlead_get_campaign_analytics --payload '{"campaign_id":12345678}' deepline tools execute smartlead_get_campaign_analytics_by_date --payload '{"campaign_id":12345678,"start_date":"2026-01-01","end_date":"2026-01-30"}' deepline tools execute smartlead_get_lead_statistics --payload '{"campaign_id":12345678}' ``` ### Email Accounts ```bash deepline tools execute smartlead_list_email_accounts --payload '{}' deepline tools execute smartlead_add_campaign_email_account --payload '{"campaign_id":12345678,"email_account_ids":[1,2,3]}' deepline tools execute smartlead_remove_campaign_email_account --payload '{"campaign_id":12345678,"email_account_ids":[3]}' deepline tools execute smartlead_update_email_account_warmup --payload '{"email_account_id":1,"warmup_enabled":true,"total_warmup_per_day":20,"daily_rampup":2,"reply_rate_percentage":30}' ``` ### Block List ```bash deepline tools execute smartlead_add_domain_block_list --payload '{"domain_block_list":["competitor.com","spam@bad.org"]}' deepline tools execute smartlead_get_block_list --payload '{"limit":50,"filter_email_or_domain":"example.com"}' ``` ### Smart Delivery ```bash deepline tools execute smartlead_get_delivery_test --payload '{"test_id":1}' # smartlead_create_delivery_test and smartlead_get_delivery_score are disabled pending live contract verification. ``` ### Smart Senders (Domains) ```bash deepline tools execute smartlead_search_domain --payload '{"domain_name":"example.com","vendor_id":1}' # smartlead_add_domain and smartlead_verify_domain are disabled pending live contract verification. deepline tools execute smartlead_auto_generate_mailboxes --payload '{"vendor_id":1,"domains":{"example.com":{"count":5}}}' ``` ### Generic API Request ```bash deepline tools execute smartlead_api_request --payload '{"method":"GET","path":"/v1/campaigns"}' ``` ## Response Shape Contract Deepline wraps all provider payloads in a standard result envelope: `{ data, meta }`. - `smartlead_list_campaigns` -> `result.data` is an array of Smartlead campaign objects with stable `id`, `name`, and `status` fields plus the upstream metadata returned by Smartlead. - `smartlead_get_campaign_stats` -> `result.data` contains `{ sent, opened, clicked, replied, bounced }`, mapped from Smartlead `/analytics` root counters. - `smartlead_push_to_campaign` -> `result.data` contains `{ pushed, failed, results }`. - `smartlead_create_campaign` -> `result.data` includes `{ ok, id, name, created_at }`. - `smartlead_export_campaign_leads` -> `result.data` is raw CSV text. - `smartlead_list_campaign_leads` -> `result.data` is a paginated list of lead objects. - `smartlead_get_campaign_analytics_by_date` -> `result.data` contains daily stats breakdown. ## Gotchas - **`min_time_btw_emails` vs `min_time_btwn_emails`:** The schedule endpoint accepts both field names. `min_time_btw_emails` is canonical; `min_time_btwn_emails` is the legacy alias. At least one must be provided. The validator normalizes the legacy alias to the canonical field. - **Campaign status values:** Only `PAUSED`, `STOPPED`, and `START` are valid. Note it is `START`, not `STARTED` or `RUNNING`. - **Timezone handling:** `update_campaign_schedule` requires an IANA timezone string (e.g. `America/New_York`). The schedule uses 24-hour `HH:MM` format for `start_hour` and `end_hour`, and `start_hour` must be strictly before `end_hour`. - **Days of the week:** 0 = Sunday through 6 = Saturday. At least one day is required. - **Sequence ordering:** `seq_number` values must be contiguous starting at 1 (no gaps). The first sequence step (seq_number 1) must have a subject line (either at step level or on every variant). - **Delay units:** `delay_in_days` is in days (0 = immediate). Do not pass hours or minutes. - **Lead batch limits:** Maximum 400 leads per `push_to_campaign` call. Use `lead_list` as the canonical field; Deepline still accepts `leads` as a compatibility alias. Duplicate emails within a batch are rejected. Emails are automatically lowercased for deduplication. - **Provider rate limit:** Smartlead currently documents 60 requests per 60 seconds per API key. Avoid high parallelism on campaign mutations unless you add queueing or backoff. - **Analytics date range:** `get_campaign_analytics_by_date` enforces a maximum 30-day window between `start_date` and `end_date`. Dates must be in `YYYY-MM-DD` format. - **Track settings normalization:** `update_campaign_settings` accepts either a single string or an array for `track_settings`. Valid values: `DONT_TRACK_EMAIL_OPEN`, `DONT_TRACK_LINK_CLICK`, `DONT_TRACK_REPLY_TO_AN_EMAIL`. - **Stop lead settings:** Valid values: `REPLY_TO_AN_EMAIL`, `CLICK_ON_A_LINK`, `OPEN_AN_EMAIL`. - **Block list entries:** Each entry must be a valid domain (`example.com`) or email address (`user@example.com`). URLs with protocol prefixes or paths are rejected. - **Lead fetch by email:** Smartlead returns HTTP `200 {}` for missing or malformed emails. Deepline converts that upstream empty-object case into `no_result`. - **Campaign unsubscribe:** Smartlead can return HTTP `200 {"ok":false}` for an unknown lead. Deepline treats that as an explicit failure rather than a successful unsubscribe. - **Campaign lead export:** The export endpoint returns `text/csv`, so downstream steps should parse `result.data` as CSV text, not JSON. - **Webhook event types:** `EMAIL_SENT`, `EMAIL_OPEN`, `EMAIL_LINK_CLICK`, `EMAIL_REPLY`, `LEAD_UNSUBSCRIBED`, `LEAD_CATEGORY_UPDATED`. - **Campaign IDs:** Accepted as integer or numeric string. Non-numeric strings are rejected. - **Reply thread:** `reply_email_time` must be a full ISO datetime (e.g. `2026-01-15T09:30:00.000Z`). Date-only strings are rejected. - **Client permissions:** Only `reply_master_inbox` and `full_access` are valid. Client passwords must be at least 8 characters. - **API key auth:** Pass `SMARTLEAD_API_KEY` environment variable. The API key is appended as a query parameter by the integration layer. -
snowflake.md 768 B
Use Snowflake when the task requires querying an organization's warehouse data. Prefer `snowflake_run_semantic_query` when a saved semantic layer exists or the user asks for business metrics, dimensions, filters, funnels, or model-defined entities. The semantic query tool renders the stored Snowflake semantic layer into SQL and returns both rows and the rendered SQL for inspection. Use `snowflake_run_query` only when the user provides raw SQL, asks for direct SQL, or the semantic layer does not cover the requested analysis. Leave `write` unset for read-only `SELECT` or `WITH` SQL. Set `write: true` only when the user intentionally wants a single statement that may modify or overwrite customer data. Never expose Snowflake warehouse spend as Deepline spend. -
socrata.md 3.2 KB
# Socrata Open Data Query any Socrata-hosted government open-data portal. Free, keyless, and the same query grammar everywhere. ## What this covers Socrata powers a large share of US city, county, and state open-data portals. Common dataset families: - Building permits and construction approvals - Business licenses and registrations - Restaurant and food-safety inspections - Code enforcement and property violations - 311 service requests Coverage is per-portal, not national. There is no single "US building permits" dataset — each city publishes its own. Confirm a metro is on Socrata before promising coverage for it. ## Order of operations 1. `socrata_discover_datasets` — search the cross-domain catalog by keyword. Returns the 4x4 id, the hosting domain, and the column list for each match. 2. `socrata_describe_dataset` — read the exact column names and types before writing SoQL. Column names are per-dataset and rarely what you would guess: NYC DOB permits uses `bin__`, `job__`, and `permittee_s_business_name`. 3. `socrata_query_dataset` — run the query. Skipping step 2 is the most common way to waste calls. A wrong column name returns a 400 (`query.soql.no-such-column`), not an empty result. ## Addressing Every dataset-scoped call needs **both** `datasetId` and `domain`: ``` datasetId: ipu4-2q9a domain: data.cityofnewyork.us ``` The domain is a routing input — it selects the host, and is not forwarded as a query parameter. Passing a 4x4 without its domain is rejected at validation. ## SoQL notes - `$select=count(*)` is the cheapest way to size a dataset before pulling it. - `$where` uses SQL-ish syntax with single-quoted literals: `borough='BRONX' AND issuance_date > '2026-01-01'`. - `$limit` is capped at 50,000 per request. Socrata itself will happily stream a 270 MB response to an uncapped request; the cap fails loudly instead so you paginate deliberately. - `$order` defaults to `:id` when unset. Do not remove it when paginating — Socrata's row order is otherwise unstable across pages, which silently duplicates and drops rows. ## Joining to accounts Most of these datasets key on **address**, not on a company identifier. Address normalization is the main correctness risk; expect meaningful match loss when joining raw address strings to a CRM. Where a dataset exposes a parcel or building identifier (NYC's `bin__`, or `bbl`), prefer it over the address. Owner and contractor business names are present in permit data and are usable for fuzzy company matching, with the usual legal-suffix caveats. ## Not every portal is Socrata Some government portals run CKAN or ArcGIS instead and will not respond to these endpoints. `data.ca.gov`, for example, is CKAN and returns an entirely different payload shape. Use `socrata_discover_datasets` to confirm a dataset is on the Socrata network before building against it. ## API version and credentials This connector uses keyless SODA 2.1. Upstream now identifies SODA 3.0 as the latest API; its query endpoint requires authentication or an application token. Do not substitute SODA 3.0 paths into these tools. There is no managed credential environment variable for this connector, and optional upstream `X-App-Token` headers are not currently exposed by it. -
sumble.md 994 B
# Sumble Use Sumble only when the workspace has connected its own Sumble API key. Deepline does not provide a managed Sumble key, does not pay Sumble provider spend, and should not recommend Sumble as a default fallback when no Sumble key is configured. Good fits when a Sumble key is present: - company discovery and account enrichment - people discovery, person enrichment, and related-people lookups - job-post search and job-related contacts - organization signals and priority signals - saved organization/contact list management inside the user's Sumble account Prefer the synchronous people endpoints: `sumble_find_people`, `sumble_enrich_person`, and `sumble_find_related_people`. Sumble v8 makes `sumble_people` and `sumble_person_detail` asynchronous start/poll endpoints; Deepline keeps those disabled until a Sumble polling runtime is implemented. Avoid Sumble when the caller has not explicitly connected or supplied a Sumble API key. Pick Deepline-managed providers instead. -
theirstack.md 3.9 KB
# TheirStack Agent Guidance ## Decision Framework **Use TheirStack when:** - Crustdata is unavailable or rate-limited - You need tech-stack-driven company discovery (TheirStack's core strength) - You need job posting data as a hiring intent signal - You need to enrich a known company's tech stack **Do not use TheirStack when:** - You need to filter by MX/email provider such as Microsoft 365, Office 365, Google Workspace, or Gmail. `theirstack_company_search` has no `company_email_provider` filter. Use `prospeo_search_company` with `company_email_provider` when the MX provider value is supported, or use `company_technology` and post-filter returned `email_tech.mx_provider` evidence. **Prefer Crustdata when:** - You need person/contact enrichment (TheirStack has no people data) - You need PersonDB search ## Operation Sequence 1. **Validate keyword slugs first (free):** Use `theirstack_catalog_keywords` to look up the correct keyword slug (e.g., `react`, `salesforce`, `hubspot`) before running a paid company or job search. 2. **Company discovery:** Use `theirstack_company_search` with `company_keyword_slug_or` or `company_keyword_slug_and`. For precision, use `_and`. For broad reach, use `_or`. 3. **Hiring signals:** Use `theirstack_job_search` to find companies actively hiring for specific roles or technologies. Always provide `posted_at_max_age_days` or a company filter — the API requires at least one. 4. **Count before a large search:** Use `theirstack_job_search` with `include_total_results: true` and `limit: 1`; omit `blur_company_data`. A matching count returns and bills one job (1 TheirStack API credit); no matches return and bill zero jobs. Totals require TheirStack to scan the full matching dataset, so broad filters or long date windows can take up to two minutes. 5. **Tech stack enrichment:** Use `theirstack_technographics` for a single known company — provide `company_domain` when possible (most reliable identifier). 6. **Check credits:** Use `theirstack_credit_balance` before large batch runs. ## Key Filters - **Keyword slugs** use kebab-case (e.g., `react`, `node-js`, `salesforce`). Use `theirstack_catalog_keywords` to find the exact slug first. - **Field names differ by endpoint:** `theirstack_company_search` and company-level filters on `theirstack_job_search` use `company_keyword_slug_*`; job text filters on `theirstack_job_search` use `job_keyword_slug_*`; `theirstack_technographics` uses `keyword_slug_or`. - **No email-provider filter:** do not invent `company_email_provider`, `email_provider`, `mx_provider`, or similar fields for TheirStack company search. - **Country codes** are ISO 2-letter (e.g., `US`, `GB`, `DE`). - **Funding stages:** `seed`, `series_a`, `series_b`, `series_c`, `growth`, `ipo`. - **Job seniority:** `senior`, `junior`, `manager`, `director`, `vp`, `c_level`. ## Cost Awareness - Company search is billed per company returned and is the most expensive of these operations. Use `limit: 10` for exploration. - Job search is billed per job returned and is cheaper per row. Safe to use with `limit: 25`. - Technographics is billed per company lookup, regardless of result count. - Catalog keywords and credit balance: free. - Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. ## Common Mistakes - Forgetting to provide a time filter OR company filter on job search → API returns error - Using display names for keywords instead of slugs → no results - Reusing `keyword_slug_or` from technographics in `theirstack_company_search` → use `company_keyword_slug_or` instead - Using Prospeo-style MX filters such as `company_email_provider` in `theirstack_company_search` → TheirStack does not support email-provider filtering - Setting `limit` too high on company search → expensive - Fetching years of job results in one request → use non-overlapping windows of 180 days or less; keep `include_total_results: false` when totals are not needed -
theswarm.md 1.4 KB
# The Swarm Use the workspace's own The Swarm API key. Deepline does not bill for The Swarm usage; the connected account's The Swarm plan and API permissions still apply. Search returns profile or company IDs; fetch retrieves records for IDs, LinkedIn names, or numeric/entity identifiers supported by that action. V3 fetch uses `linkedin_names`, not the similarly named aliases found in some prose documentation. Fetch batches allow at most 1,000 identifiers. Search uses Elasticsearch Query DSL; when `stable_pagination` is true, the provider documentation requires `limit: 1000`. Use `linkedinName` for beta profile/company posts and refreshed profile requests. Pass the returned post's `urn` into comments, reactions, or reshares. Request and response pagination names differ by endpoint; use its exact contract. Company posts use `page`, profile posts use `paginationToken`, and V3 search/relationships use `pagination_token`. Relationships return connection information scoped to the API-key team. Live responses confirm an object containing `items`, `count`, `total_count`, and an optional `pagination_token`. Provider responses are preserved without flattening or renaming their fields. Partner mapping, child-team switching/creation, and connector addition are unavailable in this connector. Connector addition is asynchronous but has no documented completion-status API; it is not safe to claim completed mapping from HTTP 202 acceptance. -
trestle.md 2.9 KB
# Trestle Workflow Guidance Trestle has two phone APIs: | Tool | Use when | | -------------------------- | -------------------------------------------------------------------------------------- | | `trestle_phone_validation` | You need line type, carrier, activity score. No identity matching needed. | | `trestle_real_contact` | You need to verify the phone belongs to a specific person (name_match, contact_grade). | ## trestle_phone_validation (Phone Intel API) **Use as the default validation step after phone enrichment.** It's cheap and tells you: - `is_valid` — is this a real phone number? - `line_type` — Mobile, Landline, FixedVOIP, NonFixedVOIP, etc. - `carrier` — service provider name - `activity_score` — 0-100, where 70+ is active and 30 or below is stale/disconnected - `is_prepaid` — prepaid account status **When to use:** - Post-waterfall validation after `contact_to_phone_waterfall` - Before cold calling to filter out disconnected/landline numbers - When you don't have or don't care about name matching **Does NOT require a name.** Just pass the phone number. Phone Intel returns the line type as `line_type`. The Real Contact API returns the same signal in its flat dotted-key response as `phone.linetype`. ## trestle_real_contact (Real Contact API) **Use when identity verification matters.** Returns everything from Phone Intel plus: - `phone.name_match` — does the phone belong to this person? - `phone.contact_grade` — A (high confidence) through F (drop) - Optional email cross-validation in the same call **When to use:** - High-value outbound where you need to confirm identity - When you have a name + phone and want to verify they belong together - When you need email validation bundled with phone validation **Requires both `phone` and `name`.** Optional: `email` for cross-validation. ## Key signals - **`activity_score >= 70`** indicates an active line - **`activity_score < 30`** is stale/disconnected — auto-fails waterfall validation - **`line_type = "Mobile"`** is the outbound-friendly type - **`contact_grade A/B`** = safe to call, **D/F** = drop ## Waterfall integration When used after phone enrichment providers, Trestle validation tools automatically fail the waterfall step if: - `is_valid` is false (invalid number) - `activity_score` is below 30 (stale/disconnected) This causes the waterfall to continue to the next phone provider, ensuring you only get validated, active phone numbers. ## Billing - Both endpoints are post-deduct — you only pay on success. The normalized response uses dotted keys at `data`, for example `toolResponse.raw.data["phone.is_valid"]` and `toolResponse.raw.data["phone.activity_score"]` in CLI JSON output. These keys are literal property names; do not try to read them as nested `phone.is_valid` objects. -
twitterapi.md 2.1 KB
# TwitterAPI.io Guidance Managed X/Twitter data API. Auth is a provider-level `X-API-Key`; all operations are GET reads. Use these instead of scraping x.com. ## Picking an operation - Tweet search: `twitterapi_advanced_search` (include `since:YYYY-MM-DD` in the query for last-30-days research). Community-wide search: `twitterapi_community_search`. - A profile: `twitterapi_user_info` (by userName) or `twitterapi_batch_user_info` (many userIds at once) or `twitterapi_user_about` (extended profile). - A user's tweets: `twitterapi_user_last_tweets` or `twitterapi_user_timeline`. - Audience: `twitterapi_user_followers` / `twitterapi_user_followings` (full profiles, pageSize 20-200) or `twitterapi_user_followers_ids` (IDs only, up to 5,000/page — far cheaper for large audiences) or `twitterapi_verified_followers`. - Relationship: `twitterapi_check_follow_relationship`. User discovery: `twitterapi_user_search`. - Around a tweet: `twitterapi_tweet_replies` (or `_v2` for ranked), `_quotations`, `_retweeters`, `_thread_context`. Hydrate known IDs: `twitterapi_tweets_by_ids`. Long-form: `twitterapi_article`. - Lists: `twitterapi_list_timeline`, `twitterapi_list_followers`, `twitterapi_list_members`. - Communities: `twitterapi_community_info` / `_members` / `_moderators` / `_tweets`. - Trends: `twitterapi_trends` (needs a WOEID). Spaces: `twitterapi_space_detail`. - Account/credits: `twitterapi_my_info`. ## Pagination Paginated endpoints take a `cursor` (omit or "" for the first page) and return `has_next_page` + `next_cursor`. Loop on those to page; do not refetch page one. ## Cost Every request carries a minimum charge, applied even when it returns zero results. Follower/following/ID pulls are billed per returned item and can get large fast — prefer `twitterapi_user_followers_ids` for big audiences, and bound pages/time windows before fanning out. `article`, `check_follow_relationship`, and `community/info` are fixed higher-cost calls. Deepline credit pricing for these actions is generated from the provider pricing metadata and rendered on the public provider pages. -
upcell.md 715 B
# Upcell Use Upcell when the workflow needs mobile phone availability checks or matched mobile phone reveal for a known person. Prefer `upcell_contact_existence` first when the user only needs to know whether a mobile exists, because it is free. Use `upcell_enrich_contact` when the mobile number itself is needed. For both actions, provide at least one strong matcher: - `linkedinUrl` - `email` - `personalEmail` - `firstName`, `lastName`, `title`, and `companyName` - `firstName`, `lastName`, and either `companyDomain` or `companySocialUrl` Only mobile workflows are currently enabled. Email, social URL, and usage stats endpoints are intentionally not exposed until pricing and availability are confirmed. -
versium.md 690 B
# Versium Use Versium for US consumer and business enrichment. If the workspace connects its own API key, that key takes precedence and Deepline does not bill the provider usage. Otherwise, actions with complete match-credit pricing use Deepline-managed access. Contact Append and Predictive Scores still require a workspace key because the supplied pricing table does not fully price them. Choose the narrowest action and output set that answers the request. Contact, demographic, audience, and predictive-score actions require at least one person identifier. Firmographic append requires a company identifier. Treat hashed email input as sensitive data even though it is not plaintext. -
wiza.md 3.7 KB
# Wiza — Agent Guidance ## When to use Wiza for LinkedIn → email/phone enrichment. Key advantage over ContactOut: **accepts Sales Navigator and LinkedIn Recruiter URLs** in addition to standard LinkedIn profile URLs. Strong for outbound teams with Sales Nav lists. Wiza is backed by the upstream OpenAPI contract. Prefer OpenAPI-native payloads for new work; Deepline still accepts older aliases and normalizes them before the provider request. ## Provider characteristics - **Input required**: LinkedIn URL (including Sales Nav), email, or name+company - **Geographic coverage**: Global - **Cost profile**: profile-only is cheapest, email costs more, and phone is the most expensive; `full` combines email and phone and costs the most - **Enrichment levels**: `none` (profile only), `partial` (email), `phone` (phone numbers), `full` (email + phone) - **Async**: reveals are queued and processed — the handler polls until finished ## Key operations ### wiza_reveal_person Starts an async enrichment job and polls until finished. Accepts any LinkedIn URL type. ```json { "individual_reveal": { "profile_url": "https://www.linkedin.com/in/johndoe" }, "enrichment_level": "partial" } ``` For personal emails only: ```json { "individual_reveal": { "profile_url": "https://www.linkedin.com/in/johndoe" }, "enrichment_level": "partial", "email_options": { "accept_personal": true } } ``` Wiza does not document an exact spend response header for reveals. The terminal GET result includes `credits.api_credits.total`; Deepline uses that exact result-body value when present. If a terminal result lacks Wiza's credits object, settlement falls back to returned contact fields, not just the requested enrichment level. For phones + emails: ```json { "individual_reveal": { "profile_url": "https://www.linkedin.com/in/johndoe" }, "enrichment_level": "full" } ``` Name + company fallback: ```json { "first_name": "John", "last_name": "Doe", "company_domain": "acme.com", "enrichment_level": "partial" } ``` ### wiza_search_prospects Discover prospects by job title, level, company, industry, location. **Free** — returns masked profiles without contact info. Returns up to 30 results per search. ```json { "filters": { "job_title": [{ "v": "VP of Sales", "s": "i" }], "job_title_level": ["VP"], "company_industry": [{ "v": "SaaS", "s": "i" }], "location": [{ "v": "United States", "b": "country", "s": "i" }] } } ``` Typical flow: search → get LinkedIn URLs → feed into `wiza_reveal_person` to enrich. ## Output shape `wiza_reveal_person` returns a flat object. Email at `email`, phones at `phone_number1`, `mobile_phone1`. Status at `status` ("finished" | "failed"). `wiza_search_prospects` returns `{ prospects: [...], total: N }`. ## Enrichment levels | Level | Returns | | --------- | ------------------------------ | | `none` | Profile data only | | `partial` | Emails only | | `phone` | Phone numbers only | | `full` | Emails + phones | ## Anti-patterns - Don't use `enrichment_level: "full"` on large lists without budgeting phone credits separately - Don't use Wiza defaults for a personal-email-only workflow; pass `email_options: "personal"` to avoid work/generic email lookup. - Don't skip polling — reveals are async, status starts as "queued" - Don't expect more than 30 results from search per call - Don't send a bare location string in new code; use `{ "v": "...", "b": "city|state|country", "s": "i" }`. Legacy `person_location: "New York"` remains accepted and becomes `{ "v": "New York, New York, United States", "b": "city", "s": "i" }`. -
wizleads.md 1.8 KB
WizLeads is opt-in and fallback-only. Do not add it to a workflow unless the user explicitly requests WizLeads or a preceding provider-specific step already created a WizLeads task. Preferred alternatives: - Work-email recovery: use the name + domain work-email play. - Email verification: use `leadmagic_email_validation`, then `zerobounce_validate`. - LinkedIn company URL lookup: use a company identity resolver. Use `wizleads_get_company_linkedin_id` only when a downstream API needs the numeric LinkedIn company ID. - Ordinary people or company discovery: use the dedicated discovery tools. `wizleads_scrape_salesnav` is only for a supplied Sales Navigator URL. Use `wizleads_find_email`, `wizleads_verify_email`, and `wizleads_get_company_linkedin_id` only within those endpoint-specific boundaries. WizLeads allows 10 requests per second across the provider account. Treat that as queue guidance when planning multi-step runs, especially SalesNav scrape + polling workflows. Use `wizleads_scrape_salesnav` for Sales Navigator scraping. By default Deepline waits briefly for the task to finish and returns task detail if ready. If it returns `status: "running"`, keep the returned `task_id` and poll `wizleads_get_task` until status is terminal. The scrape call opens async billing and reconciliation uses task detail counts, so do not charge polling reads separately. Use the public Deepline pricing summary returned by tools metadata when explaining cost. Relevant task flags are `inputs.useAccountless` and, for `salesnav-profile` only, `inputs.enrichEmails`. The public UI mentions Company Followers and Group Members, but the current OpenAPI snapshot does not expose those as API operations. The batch CSV endpoints are registered but disabled until shared multipart upload support exists. -
zerobounce.md 1.2 KB
# ZeroBounce Workflow Guidance - Use `zerobounce_validate` as the final email validation gate before any outbound send. Treat `invalid`, `catch-all`, `spamtrap`, `abuse`, and `do_not_mail` statuses as non-send by default. - Always inspect `sub_status` for granular failure reasons; `status` alone is not sufficient. For example, `do_not_mail` + `role_based` may still be acceptable for account-based campaigns whereas `do_not_mail` + `disposable` never is. - Use `zerobounce_batch_validate` for bulk verification of 1–100 emails. Play maps automatically batch `zerobounce_validate` calls. Results may take up to 70 seconds; an `unknown` verdict is not a validated email. - Use `zerobounce_email_finder` only for person-level lookups. `domain` is required, and at least one of `first_name` or `last_name` must be present. - Use `zerobounce_domain_search` for domain-only pattern discovery. It preserves the legacy Deepline public contract while mapping to the shared v2 `guessformat` endpoint. - Use `zerobounce_activity_data` to check recent engagement before re-engaging cold contacts. An `active_in_days` over 365 suggests the address may be abandoned. - When the `did_you_mean` field is non-empty, consider prompting the user or auto-correcting before sending. -
zoho_crm.md 630 B
# Zoho CRM agent guidance Use the module and field metadata actions before creating or updating records; Zoho custom modules and fields are workspace-specific. Prefer read actions to resolve IDs before writes. Treat create, update, delete, send, convert, merge, mass, workflow, sharing, and webhook actions as material side effects. Bulk launch actions return jobs that must be followed with the matching status and result actions. The connector is OAuth BYOK and no-bill. The connected user's permissions, selected OAuth scopes, Zoho edition, API limits, organization, environment, and data-center domain govern what succeeds. -
zoominfo.md 1.1 KB
# ZoomInfo guidance Use the five search actions to discover candidate records. Use `zoominfo_lookup` to resolve documented filter values before building searches. Send the JSON:API envelope exactly as documented: ```json { "data": { "type": "CompanySearch", "attributes": { "companyName": "ZoomInfo" } } } ``` Search results preserve the provider's `data`, `meta`, and `links` under the Deepline result envelope. Read rows from `result.data.data`, pagination metadata from `result.data.meta`, and pagination links from `result.data.links`. Do not unwrap or discard pagination metadata. Deepline charges no credits for ZoomInfo enrichment actions. Do not use customer credentials for testing; rely on the provider-owned OpenAPI examples unless an explicit Deepline internal/test Partner App is available. ZoomInfo tool results retain the provider response in `toolResponse.raw`, including array-valued response fields such as `company` and `familyTree`. The `company_name` convenience getter uses the first provider-reported company name; read the raw array when you need every associated company or family-tree entry.
-
-
recipes
-
account-orgchart.md 37 KB
# Account Org Chart Builder Build an interactive HTML org chart for account mapping: find decision makers, map reporting structures, identify the buying committee, and surface warm intro paths. ## When to use - User asks to "map an account" or "build an org chart" - User wants to find "who reports to X" or "decision makers at Y" - User wants to map the **buying committee / buying group / decision-making unit** (economic buyer, champion, technical evaluator, blocker) - User has a warm connection and wants to find paths through the org ## Inputs One of: - LinkedIn URL (+ optional company/domain) - Name + company name or domain - Just a company domain (maps the full GTM org) If only a name is given with no company context, ask before proceeding. ### Deal context intake Before running the waterfall, capture the deal context that changes who matters. If the user already gave enough context, proceed and do not interrogate them. If key context is missing, ask at most 1-2 critical questions before execution. | Context field | Why it matters | | ------------------------------------------ | ----------------------------------------------------------------------------------------- | | Product or service being sold | Changes the owning function, technical evaluator, and likely blocker | | Deal stage | Prospecting, single-threaded, active evaluation, or stuck deal changes the entry strategy | | Existing contacts or CRM relationships | Preserves warm paths and avoids buying data already owned | | Target function | Keeps the committee focused instead of mapping every executive | | Company size or account segment | Right-sizes the committee and prevents over-threading small accounts | | Known champion, blocker, or economic buyer | Anchors the output around real deal evidence, not title guesses | Right-size the target committee: | Segment | Target committee size | | ---------- | --------------------- | | SMB | 2-3 people | | Mid-market | 4-6 people | | Enterprise | 6-12 people | ## Two modes - pick before you start The waterfall below defaults to **company-wide mapping** (domain to every employee). But a lot of requests are actually **person-centric** ("build a 2-up / 2-down around this person"). They need different handling, and confusing them spends user budget and produces a worse chart. | Signal in the request | Mode | What changes | | ---------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | "map the GTM org at Acme", "who are the decision makers at Acme", a bare domain | **company-wide** | Run the full Section 2 waterfall. Hierarchy is inferred org-wide from title tiers. | | "build an org chart around Jane Doe", "Jane's manager and her reports", a single LinkedIn URL/person | **person-centric** | Do NOT run the full company waterfall. At a 100k-person enterprise it floods you with thousands of irrelevant people. Use the focused flow in **§2-person** below. | **Why this matters:** the company-wide waterfall is built to maximize coverage. Person-centric work needs the opposite: a tight neighborhood around one node. Running the wrong mode is the single most common way this recipe disappoints. ### The hard truth about reporting chains No data source Deepline can reach, including PDL, Dropleads, HarvestAPI, Apify, the `linkedin_scraper` family, or Sales Navigator, exposes a real "reports to" / manager field. LinkedIn does not publish reporting chains, and neither does Sales Nav. It improves _who you can find_, not _who reports to whom_. So **every reporting edge in any org chart this recipe produces is inferred, not retrieved.** That means: - The names, titles, LinkedIn URLs, and emails can be high-confidence (they come from real lookups). - The _edges between them_ are a best guess from title tier + team + location + tenure overlap. At a small startup this guess is usually right. At a 100k-person enterprise it can be ~10-20% confident, because dozens of same-title managers exist in the same city. - **Be honest about this in the output.** Put a confidence badge on every inferred edge and a one-line disclaimer ("reporting lines inferred from title/team/location, not from LinkedIn data"). Never present an inferred chain as if it were verified. A rep who trusts a wrong chain and name-drops the wrong manager on a call burns the account. ## Quick reference | Step | What | Source | Spend posture | | ---- | -------------------------- | ------------------------------------------------------------------- | ------------------------------ | | 1 | Resolve target | `leadmagic_profile_search` or `prebuilt/person-linkedin-to-email` | Metered Deepline action | | 2a | Deepline Native search | `deepline_native_search_contact` (4 title tiers) | Metered Deepline action | | 2b | Dropleads search | `dropleads_search_people` | Free or bundled when available | | 2c | HarvestAPI employee search | `harvestapi_search_leads` filtered by current company | Metered Deepline action | | 2d | Icypeas people search | `icypeas_find_people` | Metered Deepline action | | 2e | PDL gap-fill (optional) | `peopledatalabs_person_search` CXO+VP only | Metered Deepline action | | 3 | Classify + infer | Title-based seniority + tenure + recency signals + Claude reasoning | No provider action | | 4 | Generate output | HTML org chart file | No provider action | Typical run: 150-250 people found, 3-8 minutes. When the user asks about spend, show only Deepline-facing credits or estimates from the tool catalog. Do not discuss supplier-side cost structure. **Spend-safe order:** CRM first, then bundled/free sources, then broad paid sources, then surgical paid gap-fill for missing senior people only. Each step deduplicates against prior results so you do not pay to rediscover data the customer already owns. **Before running any paid source:** check your CRM for contacts already at this company. If HubSpot, Salesforce, or another CRM is connected, discover the live CRM tool contract first instead of guessing operation names: ```bash deepline tools search --categories crm --search_terms "contacts,account,company" deepline tools describe TOOL_ID_FROM_SEARCH ``` Then execute the discovered CRM contact/account query with the exact schema from `tools describe`. Pull name, title, email, phone, LinkedIn URL, account/company, owner, lifecycle or stage, and last activity when the CRM exposes them. Merge CRM results first. In the org chart, give CRM contacts a **CRM badge** and `+50` priority score, because you have verified data and a relationship. No need to re-enrich them. If no CRM is connected or no contact-search operation is available, skip this and start at Source 1. ## Pipeline ### 1. Resolve target identity This recipe orchestrates ONE account (or one person). Run the calls below directly with real values. Building org charts across a LIST of accounts, or rerunning this weekly? Author the pipeline once as a custom play ([deepline-plays.md](deepline-plays.md)) — each step below becomes one column, and the tier pulls become `ctx.runPlay('prebuilt/company-to-contact', ...)`. **If LinkedIn URL given:** ```bash deepline tools execute leadmagic_profile_search --input '{"profile_url": "'"$LINKEDIN_URL"'"}' ``` Read the target's identity off the result (prefer the documented getters from `deepline tools describe leadmagic_profile_search`): name = `first_name` + `last_name`, title = `current_position` falling back to `headline`, company = `current_company`, plus `location`. These four drive every matching signal downstream. Then resolve domain if missing: ```bash deepline tools execute exa_search --input '{"query": "'"$COMPANY"' official website", "numResults": 1}' ``` Take the registrable domain from the top result's URL — and prefer the domain a later enrichment result reports over one you inferred. **If name + company given:** resolve the LinkedIn URL first with `prebuilt/person-to-linkedin-harvestapi` (see §2-person step 2), or go straight to the company-wide waterfall with the domain. ### §2-person. Person-centric flow (2-up / 2-down around one person) Use this instead of the company-wide waterfall when the request centers on one individual. The goal is a tight neighborhood, not a roster. 1. **Resolve and verify the anchor.** Confirm the person currently works where the request claims with `harvestapi_get_profile` and read the returned `element.currentPosition` array (the key is singular). This live-verification step catches stale data. Capture their exact team/sub-function, title, location, and tenure; these are the matching signals for the rest of the flow. 2. **Find ±1 / ±2 candidates by constrained search, not full-company scrape.** Search for people at the same company filtered to the anchor's function + location + the adjacent title tiers: - **+1 (manager):** one tier up, same team, same metro. e.g. anchor is "Principal Engineer, Austin" then search "Engineering Manager" / "Senior Manager Engineering" at that company in Austin. - **+2 (director):** two tiers up, same function. - **−1 (reports):** one tier down, same team + a shared specialty signal if you have one (e.g. same sub-discipline). Use `dropleads_search_people` (free when available) and `deepline_native_search_contact` with title filters first; fall back to `exa_search` / Google-style queries for enterprises that index poorly. Keep each search scoped. You want ~3-8 candidates per tier, not hundreds. **Resolving a candidate's LinkedIn URL from a name:** don't reach for `leadmagic_profile_search` because it is for the reverse direction, hydrating a profile when you already have the URL. For name to LinkedIn URL, use the **Serper to HarvestAPI validate** pattern from the sibling [`linkedin-url-lookup`](linkedin-url-lookup.md) recipe: `serper_google_search` with a `site:linkedin.com/in` query, then validate the top hit with `harvestapi_get_profile` and a mandatory name-match gate. Or call `prebuilt/person-to-linkedin-harvestapi`, which wraps this maintained route without changing the older `prebuilt/person-to-linkedin` compatibility play. 3. **Rank the inferred edges, don't assert them.** For each candidate, score the likelihood they're the actual manager/report using the Manager prediction scoring table below (seniority gap + team match + geo + experience delta + tenure overlap). Surface the top 1-2 per tier _with their score shown as a confidence badge_. When several same-title managers tie (common at big enterprises), show them as parallel candidates rather than picking one. The rep can disambiguate. 4. **Enrich the neighborhood.** Run emails/phones only on the final shortlist via the `prebuilt/person-linkedin-to-email` play, using verified providers like Prospeo. Watch for non-obvious corporate domains (e.g. a company named "Acme Corporation" may use `@acme.io`, not `@acmecorporation.com`); take the domain from the enrichment result, don't assume it. 5. **Render** the same HTML chart as §4, but centered on the anchor with the inferred edges badged by confidence and the §"hard truth" disclaimer shown prominently. **Honest expectation:** emails and LinkedIn URLs from this flow are solid; the reporting edges at a large enterprise are a ranked guess (~10-20% on any single edge). That's the ceiling of title+geo inference without privileged data. Set the rep's expectation accordingly rather than over-claiming. ### 2. Find ALL employees (cost-optimal waterfall) > Company-wide mode only. For a single-person 2-up/2-down, use **§2-person** above instead. Running this full waterfall on a 100k-employee company buries the one neighborhood you care about. Run sources cheapest-first. Each step deduplicates against prior results - only net-new people advance. Use a descriptive, task-named working directory under `deepline/data/` so the user can find the outputs later, not a timestamped or random path: ```bash WORK_DIR="deepline/data/${COMPANY_SLUG}-orgchart" # e.g. deepline/data/ramp-orgchart mkdir -p "$WORK_DIR" echo "company_domain,company_name" > "$WORK_DIR/accounts.csv" echo "\"$DOMAIN\",\"$COMPANY\"" >> "$WORK_DIR/accounts.csv" ``` **Source 1: role-targeted contact discovery** The prebuilt covers this step — one call per seniority tier: ```bash deepline plays run prebuilt/company-to-contact --input '{"domain": "'"$DOMAIN"'", "roles": ["CEO", "CTO", "CFO", "COO", "CMO", "CRO", "Founder"], "seniority": "C-Level"}' deepline plays run prebuilt/company-to-contact --input '{"domain": "'"$DOMAIN"'", "roles": ["VP", "SVP"], "seniority": "VP"}' ``` Need exact tier control the play's contract doesn't expose (custom title filters, more tiers)? Call the underlying tool directly: ```bash deepline tools execute deepline_native_search_contact --input '{"domain": "'"$DOMAIN"'", "title_filters": [{"name": "dir", "filter": "Head OR Director OR Senior Director"}, {"name": "mgr", "filter": "Manager OR Senior Manager"}]}' ``` Expected: ~25-35 people. **Source 2: Dropleads (FREE)** ```bash deepline tools execute dropleads_search_people --input '{"filters": {"companyDomains": ["'"$DOMAIN"'"]}, "pagination": {"page": 1, "limit": 100}}' ``` Expected: +60-80 net new. **Source 3: LinkedIn employee scrape** Prefer the maintained native prebuilt for repeatable roster enrichment: ```bash deepline plays describe prebuilt/company-domain-to-linkedin-employees-harvestapi deepline plays run prebuilt/company-domain-to-linkedin-employees-harvestapi \ --input '{"domain":"acme.com","max_items":250}' ``` Use the underlying tools directly only when the prebuilt's stable employee-row shape does not fit the workflow. Resolve the company's LinkedIn page from the domain first, then search the native HarvestAPI provider after confirming the live contract: ```bash deepline tools describe harvestapi_get_company deepline tools execute harvestapi_get_company --payload '{"url":"LINKEDIN_COMPANY_URL"}' deepline tools describe harvestapi_search_leads deepline tools execute harvestapi_search_leads --payload '{"currentCompanies":"LINKEDIN_COMPANY_URL","sessionId":"STABLE_RANDOM_SESSION_ID","page":1}' ``` Generate one random `sessionId` before page 1 and pass that same value on every later page so the result set stays pinned to one upstream resource. HarvestAPI matches `currentCompanies` by company name even when given a URL or ID, so retain only rows whose `currentPositions[].companyId` equals the target `element.id` returned by `harvestapi_get_company`. Then deduplicate and write the final roster into `"$WORK_DIR/li-employees.csv"` or add it as an enrichment pass. Use Apify only if the native HarvestAPI search contract cannot express the requested roster. **Source 4: Icypeas people search** ```bash deepline tools execute icypeas_find_people --payload '{"query":{"currentCompanyWebsite":{"include":["DOMAIN"]}},"pagination":{"size":100}}' ``` Expected: +80-100 net new. **Source 5: PDL surgical gap-fill - ONLY for missing senior people** After building initial hierarchy, identify gaps (e.g., "5 Sales Directors but no VP Sales"): ```bash deepline tools execute peopledatalabs_person_search --payload '{"size":5,"query":{"bool":{"filter":[{"term":{"job_company_website":"DOMAIN"}},{"terms":{"job_title_levels":["cxo","vp"]}},{"exists":{"field":"linkedin_url"}}]}}}' ``` Pull at most five CXO+VP profiles, then dedupe. Fetch another small page only if the specific hierarchy gap remains. PDL bills every returned profile, so a broad page followed by deduplication still spends credits on duplicates. Merge all results, deduplicate by slugified name. ### 3. Classify seniority + infer hierarchy **Seniority classification (check in order, first match wins):** | Rank | Level | Patterns | | ---- | ----------- | --------------------------------------------- | | 0 | ceo | "ceo", "chief executive" | | 1 | c-level | "chief" + any (cto, cfo, coo, cmo, cro) | | 2 | evp | "evp", "executive vice president" | | 3 | svp | "svp", "senior vice president" | | 4 | vp | "vice president", "vp", "area vice president" | | 5 | sr-director | "senior director" | | 6 | director | "head of", "director" | | 7 | sr-manager | "senior manager" | | 8 | manager | "manager" | | 9 | principal | "principal", "staff" | | 10 | lead | "lead" | | 11 | senior | "senior", "sr." | | 12 | ic | everything else | **Tenure-weighted seniority adjustment:** A title alone is a weak signal. Adjust effective influence after initial classification: | Condition | Adjustment | | ----------------------------------- | ------------------------------------------------------------------- | | Manager/Director, tenure < 6 months | -1 effective rank (treat as 1 level below) | | Manager/Director, tenure >= 2 years | +1 effective rank (treat as 1 level above) | | VP/C-level, tenure < 3 months | flag as `newly_hired_exec = true` (see priority scoring) | | IC/Senior, tenure >= 5 years | flag as `long_tenured_ic = true` (informal influence, worth noting) | Tenure = months since `start_date` at current company. If `start_date` is unavailable, skip adjustment (don't guess). **For display, simplify to 4 levels** (use effective rank after tenure adjustment): - **exec**: ceo, c-level, evp, svp, vp -> color #a78bfa - **director**: sr-director, director -> color #60a5fa - **manager**: sr-manager, manager, principal, lead -> color #34d399 - **ic**: senior, ic -> color #525252 **Team consolidation (if >8 teams):** | Original | Simplified | | ------------------------------------ | ---------------- | | Executive Leadership, Office of CEO | Leadership | | Sales, GTM, Business Development | Sales | | Revenue Operations, Sales Operations | Revenue Ops | | Sales Enablement | Enablement | | Marketing, Demand Gen | Marketing | | Customer Success, Support | Customer Success | | AI, Product, Engineering, Data | Product & Eng | | HR, People, Finance, Legal | Other | Infer teams from title patterns (text after comma). ### 3b. Hiring velocity & staleness risk After classifying all people, compute per-department hiring velocity to flag stale sections of the chart: ``` For each team/department: recent_hires = count of people with tenure < 90 days total_headcount = count of people in team velocity_ratio = recent_hires / total_headcount ``` | velocity_ratio | Staleness risk | Display | | -------------- | --------------------------- | --------------------------------------------- | | >= 0.30 | HIGH, chart may be outdated | amber `⚡ Growing fast` badge on team sidebar | | 0.15 - 0.29 | MEDIUM, some churn expected | no badge | | < 0.15 | LOW | no badge | **Surfacing this in the UI:** - Add a `⚡ Growing fast` amber badge next to any team in the left sidebar with HIGH velocity - In the warm path banner (if shown), append: _"Note: [Team] is growing fast, contacts may have changed recently."_ - In the stats bar, add a clickable chip: `"N new hires (<90 days)"` that filters to recently-joined people This tells the rep: if Sales has 35% new hires, the org chart you're looking at is probably 30-60 days stale for that team. Re-verify before a big send. ### 3c. Map the buying committee (what makes the chart actually sell) A static org chart of titles ages out quickly and rarely tells a rep who to call. What closes deals is a **buying-committee map**: the cluster of people involved in a purchase, each tagged with their _role in the deal_, not just their title. After classifying seniority, assign committee roles using the deal context from the intake section. **Title to committee-role mapping.** Titles are a starting signal, not proof. Assign a role to each relevant contact, then verify behavior where you can. A "champion" is defined by behavior, not title. | Role | Maps from these titles | Why they matter | | ------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | | **Economic buyer** | CFO, VP Finance, CRO, GM/BU owner, P&L-owning Director; for smaller deals the owning-function VP | Approves the spend. Not always the most senior person. | | **Champion** | RevOps/Sales Ops, Enablement, Demand Gen lead, the Director closest to the pain | Sells internally for you. Title matters least here. | | **Coach** | Friendly former user, partner contact, customer success lead, IC with strong access | Gives process guidance but may not sell internally. Do not confuse with champion. | | **Technical buyer / evaluator** | VP/Director Engineering, Architect, Director of IT, CISO, Data Privacy Officer | Can kill the deal on technical, security, or integration grounds. | | **Procurement / legal** | Procurement, Vendor Management, Legal, Privacy, Compliance | Owns commercial and policy friction late in the process. Surface early. | | **Blocker / final authority** | CISO, Head of Compliance, General Counsel, Procurement/Vendor Mgmt | Quiet, risk-driven veto. Surface early. | | **End user / influencer** | ICs in the owning function; Staff/Principal/Senior Architect | Adoption sign-off and peer consensus. | | **Executive sponsor** | Relevant C-suite/SVP (CRO, CMO, CTO, CEO) | Ties the purchase to strategy. | Example: a security sale where the **CISO champions**, the **CFO is economic buyer**, the **CTO evaluates**, and the **CEO sponsors**. Four titles, four roles, one deal. **Signals that reveal who's actually on the committee** beyond title: open **job postings** (who owns the function + current tooling pain), **recent exec hires/promotions** (new budget + mandate to switch), **technographics ownership** (who lists the tool on LinkedIn can be the real technical evaluator), **content/intent engagement**, prior CRM conversations, and discovery-call mentions ("who else is involved, who signs, who could quietly stop this"). Note in the output where the rep should confirm the committee by asking. **Hidden influencer and champion-potential signals:** | Signal | Why it matters | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | Staff, Principal, Lead, Architect, Admin, or EA title | May control technical consensus, executive access, or calendar flow | | Recently joined or promoted in the owning function | Likely to have a mandate and lower attachment to incumbent tools | | Former user, former customer employee, or prior company used the product category | Higher odds they understand the pain and can coach the process | | Long-tenured IC in the buying function | Often trusted informally even without a manager title | | Mutual investor, customer, partner, alumni, or coworker path | Raises access quality and reply probability | | Public content about the problem area | Indicates personal stake or active evaluation | Classify each committee member with these output fields: | Field | Allowed values or shape | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `committee_role` | `economic_buyer`, `champion`, `coach`, `technical_evaluator`, `procurement_legal`, `blocker`, `end_user`, `executive_sponsor`, `unknown` | | `disposition` | `champion`, `supportive`, `neutral`, `skeptical`, `detractor`, `unknown` | | `access_level` | `direct`, `warm_path`, `crm_relationship`, `indirect`, `none` | | `influence_level` | `high`, `medium`, `low`, `unknown` | | `key_concern` | One short phrase tied to title, team, job post, CRM note, or public signal | | `how_to_win` | One recommended engagement angle | | `risk_if_ignored` | One concrete risk if this person is not engaged | | `next_action` | `contact_now`, `warm_intro`, `monitor`, `ask_champion`, `do_not_contact_yet` | | `evidence` | Source names and short reason, never a bare assertion | ### 3d. Multi-threading sequence Do not tell the rep to "contact everyone." Pick a sequencing strategy based on deal context and account size. | Situation | Strategy | First contacts | Notes | | --------------------------- | ----------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- | | No known contacts | Access line | Coach or warm path, then champion, then economic buyer | Start where access is strongest, not necessarily highest title | | Active single-threaded deal | Dual track | Champion plus one technical evaluator or end user | Add coverage without surprising the champion | | Stuck deal | Power line | Executive sponsor or economic buyer plus blocker/procurement | Use when the current path cannot unblock budget or risk | | Small account | Thin committee | 2-3 people max | Avoid over-threading and creating internal noise | | Enterprise account | Layered committee | 6-12 people across owning function, technical, finance, and legal/procurement | Sequence over time, do not blast all contacts at once | Reflect this in the chart (§4): show `committee_role`, `disposition`, `access_level`, `influence_level`, and `next_action` on every relevant contact. Prioritize 3-5 immediate contacts for a normal mid-market account, then list additional people as monitor or ask-champion targets. ### 4. Generate HTML org chart **Design system (avoid AI slop):** - Dark mode: `--bg: #0a0a0a`, `--surface: #141414`, `--border: #262626` - Text: `--text: #e5e5e5`, `--text-muted: #a3a3a3` - Warm path: `--warm: #f59e0b` with `rgba(245, 158, 11, 0.15)` glow - Inter font only **Critical UX elements:** - **Warm path banner** at top if user has a connection (amber highlight, "Show warm connections" CTA) - **Priority score column** (0-100): see scoring below - **Clickable stats bar**: "234 with email (18%)" filters to email=true on click; "N new hires (<90 days)" filters to recently joined - **Team sidebar** (left): collapsed teams with counts + `⚡ Growing fast` badges where applicable, click to filter - **Table layout**: sortable by priority, columns = Priority | Name/Title | Committee Role | Disposition | Access | Team | Level | Tenure | Contact | Next Action - **Committee coverage panel**: immediate contacts, monitor contacts, and ask-champion questions - **Evidence drawer**: click a role or edge to see the source and why Deepline inferred it - **Empty state with clear action**: "No contacts match - Clear all filters" button - Keyboard: `/` to focus search, `Esc` to close modal **Data contract for the generated chart:** ```ts type OrgChartNode = { id: string; parent_id: string | null; name: string; title: string; team: string; seniority_level: 'exec' | 'director' | 'manager' | 'ic'; committee_role: | 'economic_buyer' | 'champion' | 'coach' | 'technical_evaluator' | 'procurement_legal' | 'blocker' | 'end_user' | 'executive_sponsor' | 'unknown'; disposition: | 'champion' | 'supportive' | 'neutral' | 'skeptical' | 'detractor' | 'unknown'; access_level: | 'direct' | 'warm_path' | 'crm_relationship' | 'indirect' | 'none'; influence_level: 'high' | 'medium' | 'low' | 'unknown'; priority_score: number; champion_potential_score: number; next_action: | 'contact_now' | 'warm_intro' | 'monitor' | 'ask_champion' | 'do_not_contact_yet'; key_concern: string | null; how_to_win: string | null; risk_if_ignored: string | null; contact: { linkedin_url?: string; email?: string; phone?: string; crm_badge?: boolean; }; reporting_edge: { inferred: true; confidence: 'high' | 'medium' | 'low'; evidence: string[]; } | null; role_evidence: string[]; }; ``` Keep `reporting_edge` separate from `committee_role`. Reporting lines are inferred hierarchy guesses. Committee roles are deal hypotheses based on title, context, CRM, and signal evidence. **What to avoid (AI slop tells):** - Rainbow of 13+ seniority badge colors - 14+ teams without grouping - Stats that just count things (show percentages, make clickable) - Gray text below 4.5:1 contrast ratio - Jargon badges like "ZoomInfo Likely" - Hero metrics layout with identical cards Save to `deepline/data/{company-slug}-orgchart/{company}-orgchart.html` (same descriptive working dir as the data, so the chart and its backing CSVs live together and the user can find them). ## Priority scoring ``` score = title_score + contact_score + warm_score + recency_bonus + champion_potential_bonus ``` | Factor | Condition | Score | | ---------------------- | ------------------------------------------------ | --------------------------------- | | Title | exec level | +40 | | Title | director level | +25 | | Title | manager level | +10 | | Contact | has email | +25 | | Contact | has phone | +10 | | Warm | warm connection | +30 | | **Job change recency** | newly_hired_exec = true (exec, tenure < 90 days) | **+20 bonus** | | **Job change recency** | any level, tenure < 30 days | **+10 bonus** | | Champion potential | champion_potential_score >= 45 | +15 | | Champion potential | champion_potential_score >= 30 | +8 | | Tenure (negative) | tenure < 6 months AND manager/director level | -5 (lower influence, less stable) | **Why newly-hired execs get a +20 bonus:** A new VP of Sales or CTO is actively remapping their vendor stack in the first 90 days. They have the most buying authority and the lowest incumbent loyalty. This is the highest-value outreach window in the sales cycle. Display `🆕` badge in the Name column for anyone with tenure < 90 days so reps can spot them instantly without sorting. ## Champion and hidden influencer scoring Compute `champion_potential_score` on a 0-60 scale. This score is not a claim that someone is already a champion. It tells the rep who is worth testing for champion behavior. | Factor | Condition | Score | | -------------------- | ---------------------------------------------------------------- | ----- | | Role relevance | In owning function for the deal context | +12 | | Role relevance | Adjacent function with clear stake | +6 | | Influence | Director+ in owning function | +12 | | Influence | Staff, Principal, Lead, Architect, Admin, EA, or long-tenured IC | +8 | | Accessibility | Direct CRM relationship or prior conversation | +12 | | Accessibility | Warm path through customer, investor, partner, or coworker | +8 | | Change agent | Joined or promoted in last 180 days | +8 | | Change agent | Prior company used the product category or competitor | +6 | | Personal stake | Public content, job post ownership, or CRM note tied to the pain | +8 | | Engagement potential | Has email or LinkedIn plus a specific reason to reach out | +8 | Classify score bands: | Score | Label | Action | | ----- | ------------------------- | -------------------------------------------------- | | 45-60 | High champion potential | Contact or warm-intro early | | 30-44 | Medium champion potential | Use as access line or ask current champion | | 15-29 | Low champion potential | Monitor unless they fill a required committee role | | 0-14 | Unknown | Do not over-prioritize without more evidence | Keep Coach and Champion separate. A coach can explain the process but may not have political capital. A champion has pain, influence, access, and willingness to sell internally. Mark `disposition = champion` only when evidence supports behavior, not just score. ## Manager prediction scoring ``` score = seniority_gap + team_match + geo + experience_delta ``` | Factor | Condition | Score | | ------------- | --------------------- | ----- | | Seniority gap | Exactly 1 level above | +10 | | Seniority gap | 2 levels above | +5 | | Team match | Same team | +8 | | Team match | Related (substring) | +3 | | Geo | Same city | +2 | | Experience | 3+ more years | +2 | Highest score wins. Min threshold: 5. ## When NOT to use - User already has a CSV and wants enrichment -> use enriching-and-researching.md - User needs email/phone for outreach -> this recipe maps orgs, use enrichment recipes for contact info after -
build-tam.md 1 KB
--- name: build-tam description: 'Build a Total Addressable Market list by sourcing accounts and contacts from providers like Crustdata, Dropleads, and PDL.' --- # Provider-Led Account And Contact Sourcing This skill now follows the same documentation pattern as `deepline-gtm`. ## Required read order 1. Read the phase docs for global GTM policy, approval gates, and execution defaults. 2. Read and execute the sourcing workflow at `finding-companies-and-contacts.md`. ## Where to use this Use this skill for requests like: - "we sourced 935K leads and need the last 65K by end of week" - "we exhausted most strategies and need new lead-sourcing channels" - "use Deepline + Clay together to finish remaining contact coverage" - "build a TAM/list from ICP filters, then pull contacts at scale" ## Notes - Treat `finding-companies-and-contacts.md` as the canonical workflow. - Keep this file as a thin routing layer only (no duplicated playbook content). - On completion, follow `deepline-gtm` Section 7 for proactive issue feedback and session-sharing consent. -
clay-to-deepline.md 27.4 KB
--- name: clay-to-deepline description: 'Convert a Clay table configuration into local Deepline scripts. Handles extraction (MCP or script), documentation, action mapping, script generation, and parity validation against Clay ground truth.' --- # Clay → Deepline Migration > **Deprecated recipe.** It converts Clay tables to the deprecated `deepline > enrich` surface. Convert Clay tables to custom plays instead: one > `withColumn` per Clay column, per [deepline-plays.md](deepline-plays.md). > The action-mapping tables below remain useful for choosing the equivalent > Deepline tool per Clay action. ## Choosing your migration target Every migration targets a Deepline play. The shape differs by table: | Signal in Clay table | Target play shape | | ----------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Batch rows, no triggers, one-time or manual re-runs | CSV-input play: one `.withColumn(...)` per Clay action (this recipe) | | **Function (subroutine) table**: a `f_subroutine_source` "Function inputs" field + a `write-to-cell` action | **Reusable play with a typed input contract:** input = the subroutine's declared parameter row (not a lead CSV), one `.withColumn(...)` per non-source field, output = the `write-to-cell` `data` map. Drop `write-to-cell`. See the Clay Functions section in [clay-extraction.md](../references/clay-extraction.md). | | Webhook trigger, row routing (`route-row`), CRM writes, campaign pushes | Custom play with triggers/orchestration → [deepline-plays.md](deepline-plays.md) | | Hybrid: batch enrichment + downstream push to CRM/campaign | CSV-input play first, then a second play for the push | Most Clay tables are batch tables. This recipe covers that path end-to-end; for trigger/routing tables, **Extraction and Documentation still apply** — then follow [deepline-plays.md](deepline-plays.md) with the extracted config as the source artifact. **Recognize a Clay Function before you plan the migration.** If the extract has a `type: "source"` field named "Function inputs" (`f_subroutine_source`) and a `write-to-cell` action, the table is a reusable subroutine, not a batch lead list. Its input is the caller's parameter row — every column reads `{{f_subroutine_source}}?.["<Input Name>"]`, and the `write-to-cell` `data` `formulaMap` is the output schema. Migrate it as one self-contained play with that input contract and drop the Clay-internal `write-to-cell` write-back; a Deepline play returns its columns directly. The extraction reference's **Clay Functions (subroutine tables)** section has the full signature and the input/output/body capture map — read it whenever these signals appear. --- ## §1 Extraction If you need to extract from Clay (no extract JSON provided), read [clay-extraction.md](../references/clay-extraction.md) for MCP and script-based extraction paths, API endpoints, config structure, and input data formats. The full observed Clay internal API - every endpoint, plus how to pull the 1398-action catalog with input/output schemas - is in [clay-api-surface.md](../references/clay-api-surface.md). Go there when an `actionKey` is not covered by the mapping table, when you need a table's source filter criteria, or when you want a workbook's real dependency graph instead of deriving one. If the user already provided an extract JSON or Clay export, skip to §2. --- ## §2 Phase 1: Documentation (Always First) Produce before writing any scripts. Get user confirmation before Phase 2. ### 2.1 — Table Summary | # | Column Name | Clay Action | Tool/Model | Output Type | Notes | | --- | ----------- | ----------- | ---------- | ----------- | ----- | | 1 | `record_id` | built-in | — | string | | | … | | | | | | ### 2.2 — Dependency Graph (Mermaid) ```mermaid graph TD A[record_id] --> B[clay_record] B --> C[fields] C --> D[exa_agent] D --> E[strategic_initiatives] C --> F[qualify_person] E --> F ``` Use `classDef` colors: blue = local (`run_javascript`), orange = remote API, green = AI (`deeplineagent`). ### 2.3 — Pass Plan **Column alias rule:** Derive aliases from the actual Clay column name, snake_cased (e.g. "Work Email" → `work_email`). The two structural aliases `clay_record` and `fields` are fixed — all others follow the Clay schema. Do NOT invent names from a memorized list. ```markdown | Pass | Column alias | Deepline tool | Depends on | Notes | | ---- | ---------------- | ----------------------------- | -------------- | ------------------------------------------ | | 1 | clay_record | shell fetch (`clay_curl`) | record_id | Loaded into the seed CSV before enrichment | | 2 | fields | run_javascript (flatten) | clay_record | alias is always fields | | N | <clay_col_snake> | <see clay-action-mappings.md> | <prior passes> | Alias = snake_case(Clay column name) | ``` **Function (subroutine) tables use a different pass-1.** There is no lead CSV to fetch — the input IS the caller's parameter row. Skip the `clay_record`/`fields` fetch-and-flatten passes and make pass 1 the input contract: one alias per declared parameter (from `SUBROUTINE_INPUTS`, or the distinct `{{f_subroutine_source}}?.["…"]` references when `tableSettings` is absent), sourced from the play input rather than a Clay fetch. Then one pass per non-source, non-`write-to-cell` field in dependency order. The final "pass" is the output projection = the `write-to-cell` `data` map; do not build a `write-to-cell` pass. ```markdown | Pass | Column alias | Deepline tool | Depends on | Notes | | ---- | ---------------- | ----------------------------- | -------------- | ---------------------------------------------- | | 1 | <param_snake> | play input | — | One per SUBROUTINE_INPUTS parameter | | N | <clay_col_snake> | <see clay-action-mappings.md> | <prior passes> | Non-source, non-write-to-cell fields, in order | | out | (projection) | — | prior passes | = write-to-cell `data` formulaMap; not a pass | ``` ### 2.4 — Assumptions Log State every unverifiable assumption. Get confirmation before Phase 2. ### 2.5 — Prompt Extraction **Do this before writing any prompt approximations.** Actual Clay prompt templates live in formula field cell values or in `typeSettings.inputsBinding`. **Prompt recovery priority (richest to weakest):** 1. **HAR** — bulk-fetch-records cell values rendered formula prompts verbatim. Use directly. 2. **clay-extract.py output** — `fields[].typeSettings.inputsBinding[name=prompt].formulaText` has the full prompt. Mark as `# RECOVERED FROM EXTRACT — field f_xxx`. 3. **ClayMate `portableSchema`** — `columns[].typeSettings.inputsBinding[name=prompt].formulaText`. Mark as `# RECOVERED FROM PORTABLE SCHEMA — field f_xxx`. 4. **Approximated** — reverse-engineer from outputs or user description. Mark as `# APPROXIMATED — could not recover`. **JSON schema recovery from portableSchema:** ```python import json for col in d['portableSchema']['columns']: if col['type'] == 'action': for inp in col['typeSettings'].get('inputsBinding', []): if inp['name'] == 'answerSchemaType': schema_raw = inp.get('formulaMap', {}).get('jsonSchema', '').strip('"') schema_raw = schema_raw.replace('\\"', '"').replace('\\n', '\n').replace('\\\\', '\\') schema = json.loads(schema_raw) ``` **Fix Clay formula bugs in recovered prompts:** `{{@Name}}` → `{{name}}`, `{single_brace}` → not interpolated by Deepline, `Clay.formatForAIPrompt(...)` → strip wrapper. ### 2.6 — Pipeline Architecture Verification Check actual cell values across 3+ records before counting AI passes: | Cell value | Meaning | How to replicate | | --------------------------------------- | ---------------------------- | ------------------------------------- | | `NO_CELL` | Action never fired | Build from scratch | | `"Status Code: 200"` / `{"status":200}` | HTTP/webhook action — NOT AI | `generic_http_request` or shell fetch | | `""` (empty string) | Disabled or unfired | Treat as NO_CELL | | Varied generation-shaped text | Actual AI output | `deeplineagent` | **No cell data in the extract?** Some extracts carry config only (no `exampleRecords` / `bulkFetchRecords`). You cannot run the 3-record check, so do not fake it: infer architecture from `typeSettings` (`actionKey`, `conditionalRunFormulaText`, `formulaWaterfall`) and label every architecture or parity claim **`config-inferred, unvalidated`**. Defer any conclusion that genuinely needs real cell values until you fetch records (§ `bulk-fetch-records`) or the user confirms. --- ## §3 Phase 2: Pre-flight + Play Authoring ### Pre-flight Checklist Answer these **before writing the play** based on what Phase 1 revealed. Only answer questions that apply. **Table type (check all that apply):** - [ ] **Has a provider waterfall** (several finders of the same kind chained by `conditionalRunFormulaText`) → ONE Deepline waterfall play, not one pass per provider - [ ] Has phone columns → use `prebuilt/person-to-phone`; confirm the input contract, it needs name + domain while many Clay finders take only a LinkedIn URL - [ ] Has person enrichment columns → verify with `deepline tools search "person enrichment linkedin"`. Check `inputsBinding` first: a column keyed `enrich-person` is often wired as a phone or email finder - [ ] Has email finding columns → use `name-and-domain-to-email-waterfall` as primary play - [ ] Has AI generation columns (use-ai, claygent, octave) → recover prompts verbatim (§2.5) - [ ] Has scoring/qualification columns → use ICP criteria verbatim from Clay config - [ ] Has campaign push / CRM update columns → verify with `deepline tools search "<platform> add leads"` - [ ] Has cross-table lookups → export linked table to CSV first - [ ] **Is a company intelligence table** (source = Mixrank) → use `crustdata_v3_company_search` **Security (all tables):** - [ ] `CLAY_COOKIE` in `.env.deepline` (not hardcoded), single quotes, `.gitignore`d - [ ] `output/` in `.gitignore` - [ ] HTTP calls use `generic_http_request` or the generated shell fetch script; `run_javascript` is only for local row transforms ### Output Files ``` project/ ├── .env.deepline # Clay credentials (never commit) ├── .env.deepline.example # Template — safe to commit ├── .gitignore # Excludes .env.deepline, *.csv, output/ ├── prompts/ │ └── <name>.txt # One file per AI column with source header ├── scripts/ │ └── fetch_<table>.sh # Fetches Clay records → seed_<table>.csv └── <table>.play.ts # The migrated table: one column per Clay action ``` ### Cookie Pattern (mandatory) ```bash set -a; source .env.deepline; set +a : "${CLAY_COOKIE:?CLAY_COOKIE must be set in .env.deepline}" CLAY_VERSION="${CLAY_VERSION:-v20260311_192407Z_5025845142}" clay_curl() { curl -s --fail \ -b "${CLAY_COOKIE}" \ -H "accept: application/json, text/plain, */*" \ -H "origin: https://app.clay.com" \ -H "referer: https://app.clay.com/" \ -H "x-clay-frontend-version: ${CLAY_VERSION}" \ -H "user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36" \ "$@" } ``` **Never hardcode `CLAY_COOKIE` in scripts.** Use single quotes in `.env.deepline` (GA cookies contain `$`). **How to get the cookie:** Copy a `curl` from Chrome DevTools (right-click any api.clay.com request → Copy as cURL). Extract the `-b '...'` value. ### Clay API Endpoints | What you need | Correct endpoint | Notes | | --------------- | ------------------------------------------------------- | ------------------------------------------------------------------ | | All record IDs | `GET /v3/tables/{TABLE_ID}/views/{VIEW_ID}/records/ids` | View ID required — without it returns `NotFound` | | View ID | `GET /v3/tables/{TABLE_ID}` → `.table.firstViewId` | Always fetch dynamically | | Fetch records | `POST /v3/tables/{TABLE_ID}/bulk-fetch-records` | Body: `{"recordIds": [...], "includeExternalContentFieldIds": []}` | | Response format | `{"results": [{id, cells, ...}]}` | Key is `results`; record ID is `.id` (not `.recordId`) | ### Play Shape and Column Order The migrated table is a play file: the body reads the seed CSV with `ctx.csv(...)`, chains one `.withColumn(alias, ...)` per Clay action in dependency order, and finishes with `.run({ key })`. Author it per [deepline-plays.md](deepline-plays.md); columns execute in declaration order, and each column can read any earlier alias. Validate before spending credits: ```bash deepline plays check <table>.play.ts ``` `plays check` compiles the play and reports errors without starting a run. Order columns cheapest-first: `run_javascript` transforms before paid provider calls, so a local derivation is available to the columns that depend on it. ### Referencing Columns Reference columns by alias, never by index. In a payload template, `{{alias}}` resolves to an earlier column or a seed CSV header. Interpolation walks the full path: `{{fields.title}}`, `{{fields.company.name}}`, and array indices like `{{li_serper.organic[0].link}}` all resolve. A path that does not exist renders empty rather than erroring, so check a sample row when a value comes back blank. ### Running and Waiting ```bash deepline plays run --file <table>.play.ts --input '{"csv": "seed.csv"}' --watch deepline runs export <run-id> --out output_<table>.csv ``` `--watch` blocks until the run reaches a terminal state. To inspect fill rates on the export, `deepline csv show --csv output_<table>.csv --format json --summary` returns `columnStats` at the **top level** of the JSON (alongside `total_rows`), not nested under `_metadata`. ### Piloting A Subset Of Rows Pilot on a slice of the seed CSV before the full run: ```bash head -4 seed.csv > pilot.csv # header + 3 rows deepline plays run --file <table>.play.ts --input '{"csv": "pilot.csv"}' --watch ``` To skip rows a cheaper pass already answered, gate the expensive column with `runIf` so it only fires where the value is missing: ```ts runIf: (row) => !row.work_email, ``` This replaces the old filter-to-a-separate-CSV-and-merge workaround. Tool receipts are content-addressed on tool + input, so re-running the play does not re-bill results it already bought. ### Architecture Choice: Play vs Python SDK For Claygent-heavy tables, use a **pure Python script** with `deepline tools execute exa_search` + `deeplineagent`. Enables parallel execution with `ThreadPoolExecutor`, full retry/confidence control. The play pattern still applies for non-AI passes and simple single-column `deeplineagent` enrichments — and unlike shell-assembled JSON, the play is a real TypeScript file: JS transforms live in the file directly with no quoting or escaping problems. ### Common Failure Modes | Symptom | Cause | Fix | | ----------------------------- | ------------------------------------------------ | --------------------------------------------------------- | | `{{col}}` empty in prompt | Alias declared after the column that reads it | Move the producing column earlier in the play | | Interpolation renders blank | Path does not exist on that row (`{{a.b.c}}`) | Check a sample row's actual shape; fix the path | | Unexpected re-charge | Input value changed between runs | Receipts key on tool + input; identical inputs reuse | --- ## §4 Action Mapping **Start with the job-based mapping table in [clay-api-surface.md](../references/clay-api-surface.md#map-by-job-not-by-provider-name).** Clay names one action per provider - 21 phone finders, 15 work-email finders - and those collapse into a single Deepline waterfall. Mapping provider-by-provider covers about 17% of real-table usage; mapping by job covers about 85%. Then use [clay-action-mappings.md](../references/clay-action-mappings.md) for the exact CLI payload of a specific tool. It is a payload reference, not a complete action list: it has no phone or CRM rows. Always verify tool IDs before use. For anything neither file maps, pull the action's real input schema from Clay's catalog (see [clay-api-surface.md](../references/clay-api-surface.md)), then map it. Do not guess from the action name. ### Unknown Action Fallback ```bash deepline tools search "<what the action does>" # search by intent deepline tools describe <candidate_tool_id> # inspect candidate # if nothing found → deeplineagent fallback ``` | You see in Clay | Search query | Likely result | | ---------------------- | -------------------------- | ----------------------------------------------- | | `enrich-person-with-*` | `"person enrich linkedin"` | `leadmagic_profile_search` | | `find-email-*` | `"email finder"` | `hunter_email_finder`, `leadmagic_email_finder` | | `verify-email-*` | `"email verify validate"` | `leadmagic_email_validation` | | `company-*` | `"company enrich"` | `prospeo_enrich_company` | | `add-to-campaign-*` | `"add leads campaign"` | `instantly_add_to_campaign` | ### Summary Table | Clay action | Deepline tool | | --------------------------------------------- | -------------------------------------------------------------------------------- | | Email waterfall + `validate-email` | `name-and-domain-to-email-waterfall` + `perm_fln` + `leadmagic_email_validation` | | `enrich-person-with-mixrank-v2` | `leadmagic_profile_search` → `crustdata_v3_person_enrich` | | `chat-gpt-schema-mapper` | `deeplineagent` with `jsonSchema` | | `use-ai` (no web) | `deeplineagent` | | `use-ai` (claygent + web) | Binary search optimizer — see §5 | | `octave-qualify-person` | `deeplineagent` + `jsonSchema` ICP scorer | | `add-lead-to-campaign` | `instantly_add_to_campaign` or `smartlead_api_request` | | `route-row` | **Not replicable.** Produce filtered output CSV per destination. | | `find-lists-of-companies-with-mixrank-source` | `crustdata_v3_company_search` + optional `prospeo_enrich_company` | --- ## §5 Binary Search Optimizer (Claygent Web Research) Use whenever replicating a `use-ai (claygent + web)` column. ### Pass Structure ```python # Pass A — parallel, highlights-only (cheap). Include domain in ALL queries. queries = [ f'"{co_name}" {domain} 10-K annual report investor relations', f'"{co_name}" {domain} new product launches announcements 2024 2025', f'"{co_name}" {domain} go-to-market new customer segments 2024 2025', ] # Pass B — synthesis with confidence gate schema = { ...fields..., "confidence": "high|medium|low", "missing_angles": [...] } # confidence == "high": STOP # Pass C — follow-up exa searches on missing_angles[0:2], text=True # Pass D — re-synthesize # Pass E — primary-source deep-read via _extract_primary_source_url(company_domain) ``` Always add `research_confidence` and `research_passes` tracking columns. ### Confidence Calibration (26-row data) - `high`: 0% — essentially never with Exa - `medium`: 35% — large public companies, funded startups - `low`: 65% — but 50% of `low` had specific useful content `low` ≠ bad output. Use `is_failed_research()` content quality check instead: ```python FAILURE_MARKERS = ['UNCHANGED', 'UNRESOLVED', 'NO UPDATE', 'SOURCE INVALID', 'CRITICAL SOURCE MISMATCH', 'Unable to determine'] ``` Expected failure rate: ~15%. ### Known Failure Modes | Failure | Example | Fix | | ----------------- | ------------------------------- | -------------------------------------------------------- | | Name collision | `onit.com` → wrong company | Quote `co_name`; add domain | | No indexed source | `ziphq.com` | Fall back to Crunchbase + LinkedIn | | URL contamination | Deep-read returns wrong company | Use `_extract_primary_source_url(company_domain=domain)` | ### Adapting Search Angles | Use case | Angle A | Angle B | Angle C | | ------------------- | -------------------- | ------------------- | ---------------- | | GTM strategy | 10-K / IR | Product launches | New segments | | Signal detection | Tech stack / jobs | Engineering blog | Conference talks | | Competitor research | Pricing pages | G2/Capterra reviews | Exec interviews | | Private company | Crunchbase / funding | Newsroom | Founder blog | --- ## §6 Patterns and Antipatterns Clay-specific patterns. For general Deepline patterns (email plays, interpolation, deeplineagent, column shapes), follow `enriching-and-researching.md` from the deepline-gtm. ### Prompt Recovery **Do this**: Extract from richest source (HAR > extract > portableSchema > approximate). Mark files with source header. **Not this**: Approximate when the actual prompt was in the export. ### Email Match Rate | Format | % of Clay emails | | ----------------------- | ---------------- | | `fn.ln@domain` | 63% | | `fln@domain` | 19% | | `fn@domain` | 3% | | Provider waterfall only | ~12% | Use `name-and-domain-to-email-waterfall` as primary play. Accept `valid`, `valid_catch_all`, AND `catch_all` from validation (NOT `unknown`). ### Cookie Security Read `CLAY_COOKIE` only in the generated shell script. Single quotes belong in `.env.deepline`. Add `.env.deepline` and `output/` to `.gitignore`. Never place the cookie in a play payload. ### run_javascript `run_javascript` does not expose fetch or process.env. Use it for deterministic row transforms only. Route HTTP through `generic_http_request`, or fetch Clay records in `scripts/fetch_<table>.sh` with `clay_curl`. Column payloads live in the play file as real TypeScript, so no shell JSON assembly is needed. ### Clay API Calls Always use `clay_curl` wrapper. Get `VIEW_ID` from `.table.firstViewId`. Parse with `.get('results', [])`. Record ID is `.id` not `.recordId`. --- ## §7 Phase 3: Validation ### Parity Thresholds Base thresholds: | Field type | Threshold | | ---------------------------------------- | ------------------------------------- | | Deterministic (formulas, fetch, scoring) | 100% exact match | | LLM classification | ≥90% exact match on unambiguous cases | | LLM generation | Tone and intent match (manual review) | Clay-specific extensions: | Field type | Threshold | | ------------------------------------------- | -------------------------------------------- | | Email (`work_email`) | DL found rate ≥95% of Clay found rate | | Structured (`deeplineagent` + `jsonSchema`) | All schema fields populated in 100% of rows | | Web research | `is_failed_research()` False on ≥85% of rows | ### Running the Comparison ```bash python3 /path/to/skill/scripts/compare.py ground_truth.csv enriched.csv python3 /path/to/skill/scripts/compare.py ground_truth.csv enriched.csv \ --map '{"clay_final_email":"work_email","clay_job_function":"job_function"}' ``` ### Accuracy Expectations - **Valid/valid_catch_all**: high confidence (<5% bounce) - **catch_all**: domain accepts all — best guess. Same limitation as Clay (same ZeroBounce under the hood) - **unknown**: skip, do not treat as found ### Diagnosing LLM Mismatches Use this mismatch process: check prompt parity → check model parity → check true ambiguity (run 3x) → document, don't overfit. --- ## §8 Critical Rules - **Declaration order is execution order**: put `run_javascript` transforms before the paid columns that read them - **Gate expensive columns with `runIf`**: never pay for a row a cheaper pass already answered - **Flatten first**: a `run_javascript` column that flattens `clay_record` before `{{fields.xxx}}`. Not needed in Python SDK — use `json.loads()` directly - **Interpolation walks the full path**: `{{col.field.nested}}` and `{{col.items[0].field}}` both resolve; a missing path renders empty, not an error - **Structured JSON for deeplineagent**: Single invocation per column, all fields in one `jsonSchema` - **Cookie in env**: Never embed `CLAY_COOKIE` in play code or payloads; read it only from `.env.deepline` in the generated shell fetch script - **Catch-all is valid**: Accept `valid`, `valid_catch_all`, `catch_all`. NOT `unknown` - **Prompts verbatim**: Use exact text from source — small differences cause systematic drift --- ## §9 Migration Checklist 1. **Extraction (§1)**: Extract Clay table config (or skip if user provides extract) 2. **Phase 1 (§2)**: Table summary, dependency graph, pass plan, prompt extraction, assumptions 3. **Confirm**: Get user approval on assumptions and pass plan 4. **Phase 2 (§3)**: Pre-flight → write `fetch_<table>.sh` + `<table>.play.ts` 5. **Pilot gate**: `deepline plays check` (compiles, no spend), then a 3-row pilot CSV (real APIs) 6. **Full run**: After pilot approval 7. **Phase 3 (§7)**: `compare.py ground_truth.csv enriched.csv` — confirm thresholds pass 8. **Trigger/routing migration** (optional): If table needs triggers/routing → [deepline-plays.md](deepline-plays.md) ### Pilot Gate `run_javascript` needs no pilot. For paid tools, compile first, then run 3 rows, in that order: ```bash deepline plays check <table>.play.ts # step 1: compile only, no spend head -4 seed.csv > pilot.csv deepline plays run --file <table>.play.ts --input '{"csv": "pilot.csv"}' --watch # step 2: 3 rows, real providers deepline plays run --file <table>.play.ts --input '{"csv": "seed.csv"}' --watch # step 3: all rows ``` `plays check` compiles the play without spending credits. It still calls the Deepline compile API, so it needs auth and network — it is not an offline check. -
deepline-monitors.md 37.4 KB
--- name: deepline-monitors description: 'ACCESS-GATED beta. Deepline Monitors are provider event feeds (job posts, email replies, funding, intent) that stream into your warehouse and trigger plays. Only use if you have monitor access: run `deepline monitors status` first; if it reports no access, do NOT use this recipe — tell the user to contact the Deepline team.' --- # Deepline Monitors Monitors are **access-gated Deepline-native signal feeds**. The customer launch includes Company Radar and Contact Radar. A monitor provisions a Deepline-managed feed; events land in a Customer DB table. There is **no run to kick off** — it streams as events arrive. ## The job: turn a future signal into a useful decision A monitor is not a dashboard setting. It is a promise that a future real-world event will reach the right workflow. Earn that trust: show the route, give a small piece of evidence now, prove the stored definition says what was asked, then let live events validate delivery over time. Each step answers a different question; do not pretend one proves all four. ## User-facing communication The lifecycle below is internal. The user needs the next decision, not a monitoring log. - **Ready to turn on:** for several targets, show a short scope table; for one, use a sentence. Recommend the first filter, name the live Deepline price and delivery, then ask one yes-or-adjust question. Do not lead with domains, validation, a dry-run, monitor keys, or timing. - **Calibrating from matches:** show the actual attributed rows in a Markdown table before any roll-up or recommendation. The table is the thing the user is calibrating against—counts and labels such as “relevant” are not enough. For new hires, use `Company / Person / Joined as / When` and put a verified LinkedIn link on the person's name, not in a separate `Profile` column. After the artifact, give one concrete recommendation and one question: `Want me to use that, or adjust it?` This applies even when the first calibration call is to leave the filter broad: the user explicitly asked to calibrate. For no access, no sample, failure, or stop, state that outcome directly; do not invent a scope or results table. ## Read the deployed monitor spec first Every `deepline monitors get <key> --json` response includes `monitor_spec`. When `monitor_spec.available` is true, use its `fields` as the field list for that monitor type before you write a definition, patch, or dependent Play. Each entry names the exact `payload.*` path and carries its description, type, required flag, enum, format/pattern constraints, plus provider-specific applicability, precedence, semantics, and grammar where relevant. Never guess a monitor filter from a similar tool or a different monitor type. When `available` is false, it is a legacy monitor without a current capability spec: use the stored definition and run `monitors check` before an update. Read the stored `definition` beside `monitor_spec`: the definition tells you what this monitor currently uses; the spec tells you what each field means and which values are legal. `conditional_requirements`, when present, states fields required only for a particular payload selection. ## Default-guided setup with paid consent Treat monitor setup as **infer → validate → approve → deploy → observe**, not a sequence of choice cards. Resolve company domains from names when necessary, carry the evidence, and use the request's titles, roles, geography, source list, and destination as filter inputs. Do not ask the user to supply domains or choose routine filter values that the request already implies. Deployment, reactivation, and historical lookback can accept variable numbers of billable events. A user request is authorization to investigate and prepare the exact monitor definition, not consent to incur those charges. After the live contract, registry-reuse check, `monitors check`, and deploy dry-run, obtain explicit approval before every paid state change in every workspace. The approval must cover the targets, event signal, live Deepline price and charge basis, unknown total volume, and which current or historical calibration step it authorizes. Before consent, disclose any known downstream action and the unknown-consumer risk in one short `Delivery:` line; those can change what approval means. Keep raw monitor keys and implementation detail internal. A dry-run is read-only and does not replace this approval. For an initial forward scout, inspect safely attributed rows at 30, 60, and 90 seconds. If none arrive, leave the forward monitor active and offer a separately approved, priced 30-day historical step. Later empty historical steps can only advance to 60, then 90 days maximum. Use 30, 60, and 90 days as the calibration rungs, not an invented guarantee of an exact source cutoff. When a live contract evaluates source dates at calendar-month precision (including Deepline Native new-hire and promotion radars), show the provider-effective start date/window in the dry-run and approval request before it can be billed. "Similar companies" is a billable scope decision. Resolve and, when useful, recommend a small evidence-based candidate list, but do not deploy those candidates merely because they seem plausible. Show their names and inclusion decision in the scope table; keep resolved domains with the definition unless the user needs them. Before asking where to send matches, check Slack with `deepline notifications slack channels --json`. A successful response supplies real channels: recommend the best obvious channel, not an invented one. If Slack is unavailable or has no usable channel, say that plainly. When a CRM destination is configured, name it and ask: `Want me to turn this on and connect Slack, or send matches to <CRM>?` Otherwise ask: `Want me to connect Slack and turn this on?` Routing matches to Slack or a CRM is a downstream action; include it in the same approval as the monitor. `job_titles` accepts only the documented Deepline Native input syntax: double-quoted title terms joined with uppercase `AND`, `OR`, and `NOT`, such as `"VP" OR "Head of Sales"`. Deepline Native applies the expression upstream. Parentheses and exact title-match boundaries are not documented, so do not invent substring, word-boundary, or case-sensitivity semantics. `job_titles` overrides `departments` and `seniorities`, so do not send both forms. `updates_since` is the historical boundary for a new radar, not a filter to revise during calibration. Do not invent a title, department, seniority, or geography filter when the selected row does not list it. **Keep approval decision-ready, not procedural.** Do not turn routine monitor setup into a titled plan, choice-card questionnaire, Mermaid diagram, or a raw definition dump. Do the read-only validation and price lookup directly, then send one short approval request for the exact paid scope. If a progress update is useful, make it one plain sentence. **A dry-run is not a test.** It proves a definition can be deployed, not that a deployed monitor accepts its event shape. For an internal/test workspace, read back every monitor the user explicitly asked to create or reactivate and run the safe validation-only test whenever `monitors get` returns a `sample_payload`. In a customer workspace, run it only after the user explicitly approves that diagnostic. It writes no rows, dispatches no Plays, and does not spend event credits. Mark the monitor as tested only when the response confirms `accepted: true`, `test_mode: validation_only`, `persisted_rows: 0`, and `dispatched_bound_plays: 0`. If no sample payload exists, report that the provider offers no safe payload test and do not claim the monitor was tested. Use these defaults unless the request says otherwise: - **Scope:** for a calibration scout, use the named targets and the requested event only. Leave title, seniority, department, and geography unset unless the user explicitly supplied them. Do not broaden the target set for hypothetical future reuse. - **Time:** start forward from deployment. If there are no safely attributed rows at 90 seconds, offer a 30-day historical step; only after an explicit approval may the next empty step widen to 60, then 90 days maximum. Do not silently select history or widen it during calibration. - **Action:** preserve the monitor's event feed. Add a downstream Play only when the user asks for a notification or another side effect; otherwise leave the event in its Customer DB stream for review. - **Ambiguity:** use the available company/person context to resolve it. Keep an unresolved target with a reason rather than stopping the whole setup. Before presenting a recommendation, read the live monitor contract, build the definition, run `monitors check`, and run `monitors deploy --dry-run`. Report the actual deploy/reactivation charge, recurring charge, and/or Deepline credits per accepted event returned by those commands. Do not ask the user whether they want a more expensive option before looking up its price. When event volume is unknown, say that future total is unknown; do not invent an estimate. Ask only to complete a missing requirement: which signal matters, which targets belong in scope, or where an explicitly requested alert/action should go. Ask when a deployment check exposes an unaffordable or invalid configuration too. Put the recommended configuration and live Deepline credit impact in one short approval question. The validated dry-run explains scope and price; explicit confirmation authorizes the paid mutation. ## Step 0 — access gate (do this first) Monitors are an access-gated beta. Before any monitor command, confirm access: ```bash deepline monitors status --json ``` - **You have access** → proceed. - **No access** → stop. Do not run other monitor commands; tell the user to contact the Deepline team to request access. The response is `{ "has_access": boolean, "reason": string }`; branch on `has_access`. Other failures need diagnosis, not reinterpretation as rollout denial. ## Explain the monitor only when useful After validation or deployment, use a sentence when the user needs the model: the monitor keeps watch; a matching event enters the shared signal feed; a requested Play can then notify Slack, update a CRM, create a task, or enrich the company. Do not render a diagram or a setup summary before doing the work. The important boundary is that a monitor does not itself send Slack messages, and it does not create a private channel for one workflow. Several monitors can write to the same feed; each Play decides which new rows deserve action. That keeps one useful source of truth, but a Play filter controls downstream action only, not monitor ingestion cost. ## First proof of value: a minimally filtered scout Do not make a customer wait days to discover whether the intended signal has coverage. After approval, start the approved small scout batch with only the target and requested event filters, then prove each stored definition after the write. This is the fastest feedback loop and guards against false success. **Example outcome:** “Tell me when Stripe posts a Chief Financial Officer job, then let a Play react.” Read the live tool contract before using this example. ### Optional: test the closest real signal Offer a small live probe when it helps the customer decide whether to deploy. Do not make every monitor pretend it has one. A credible proxy has the same **thing** (job, job change, review, post) and the same **moment** (new or current) as the monitor. An adjacent lookup can be useful research, but it is not evidence that the monitor will fire. | Monitor is watching for… | Find a credible probe with… | Do not mistake this for… | | ---------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------- | | Job postings | `deepline tools search "job postings" --json` | A people or title search | | A tracked contact changing jobs | `deepline tools search "job change" --json` | The contact's current profile | | A provider webhook, campaign event, or website visit | The connected provider's test event or a deliberate test visit after deploy | A separate provider REST read | | Any other signal | `deepline tools search "<signal in plain English>" --json` | A loosely related enrichment | Read the shortlisted tool's live contract and price. Only run a one-result or one-event probe after the customer approves its cost. Prefer the same provider as the monitor when it exposes a callable read/search surface; otherwise say that no faithful preflight exists instead of inventing one. A probe tests current coverage, not future delivery. Read its identity, date, and URL before calling it a match; an empty result proves only that nothing was found now. The durable proof is a stored definition and a real delivered event. ### Verify every requested deployment After creating or reactivating a monitor, read its stored definition and run the first applicable verification below. This is execution evidence, not a proposal or a substitute for a future provider event: 1. **Safe callback diagnostic.** When `monitors get <key> --json` returns a `sample_payload`, run it through the deployed monitor without writing a row or waking a Play in an internal/test workspace. In a customer workspace, first obtain explicit approval for this diagnostic: ```bash deepline monitors test <key> '<sample_payload from monitors get>' --json ``` Require `accepted: true`, `test_mode: validation_only`, `persisted_rows: 0`, and `dispatched_bound_plays: 0`. This proves Deepline accepts that event shape against the monitor's real binding; it does not prove the upstream provider emitted it. 2. **No safe payload test.** When `sample_payload` is absent, state the test is unavailable. A current-signal probe may still help assess coverage, but it is not a monitor test and may consume credits, so keep it opt-in. Do not fabricate a provider webhook body just to fill the gap. `monitors test` is the validation-only diagnostic; it does not expose a dispatch mode. A real end-to-end event test needs its own explicit customer approval and a separately designed delivery path. ```bash # Learn the live job-opening payload fields, output stream, and event price. deepline tools get deepline_native.company_job_openings --json MONITOR='{ "key": "stripe-cfo-job-openings", "name": "Stripe CFO job openings", "tool": "deepline_native.company_radar", "payload": { "domain": "stripe.com", "radar_type": "company_job_openings", "job_titles": "\"Chief Financial Officer\"" } }' # These are safe. They validate the exact definition and show cost/reuse. deepline monitors check "$MONITOR" --json deepline monitors deploy --dry-run "$MONITOR" --json # After explicit approval of scope, shared-stream impact, and price: deepline monitors deploy "$MONITOR" --json deepline monitors get stripe-cfo-job-openings --json # In an internal/test workspace, or after explicit customer approval: deepline monitors test stripe-cfo-job-openings '<sample_payload from get>' --json ``` The deployment proof is the final `get` plus the safe test when available: the definition must show `definition.payload.job_titles` as `"Chief Financial Officer"`, the expected domain/radar type, and `status: active`; the safe test must accept the provider-shaped sample without persistence or dispatch. If any check fails, report a failed deployment. Do not wait for events to infer the filter. ## Observe and refine `updates_since` is a permanent radar boundary, not pagination. A historical step can replace an upstream radar and restart billable ingestion, so do not patch it as a casual filter update. Use the live contract and dry-run to choose the safe create/replace path, obtain approval for that exact operation, and then read the resulting definition back. The only calibration ladder is 30, 60, then 90 days; never use more than 90 days without a separate user request and a revised approval. Capture `observation_started_at` before deployment. For Deepline Native, read `data_plane_binding` from `monitors get <key> --json` and use both `_dl_monitor_id` and `_dl_monitor_binding_version` to inspect this monitor's current rows. Add `_dl_received_at` only to narrow the observation window. Without that binding, report monitor state and `last_received_event`; do not guess which shared-stream rows belong to the monitor. A missing output table or binding is an operational failure, not an empty sample. ## How to communicate Use one decision-shaped response, not a recap. Do not open with deployment, credits, timing, history, or a checklist; those matter only once a real decision is ready. Translate internal language: say “first pass,” “matches,” and “look further back,” not “forward scout,” “accepted events,” or “historical rung.” ### First-pass approval For a multi-company monitor, use this shape after live validation. Keep only decision-bearing columns; resolved domains, duplicate checks, and monitor plumbing are internal. ```markdown Recommendation: Start broad—no title, department, seniority, or location filter—so the first real matches tell us what belongs. | Watch | Company | Why | | ----- | ----------- | ---------------------------------- | | Yes | <company> | <named target or approved peer> | | No | <candidate> | <why it is not in this first pass> | Cost: <live Deepline price and charge basis>. Delivery: <recommended connected Slack channel, or Slack/CRM choice>. Want me to turn this on, or adjust it? ``` For one company, replace the table with a sentence. Similar companies are a scope recommendation, not a silent expansion: show each candidate and whether it is in this pass. Ask the delivery question only inside this one approval question; recommend a real Slack channel when one is connected, otherwise offer Slack or the configured CRM. ### Calibration decision Show every decision-bearing returned row when there are 25 or fewer. Above that, show the rows that support the boundary, state the total, and link or export the complete user-usable result set. Use only returned values. For a new-hire monitor: ```markdown | Company | Person | Joined as | When | | --------- | --------------------------------- | ---------------- | --------------- | | <company> | [<person>](verified-linkedin-url) | <returned title> | <returned date> | Recommendation: <keep broad, or name the exact title/signal patterns to include and exclude>. Why: <the pattern visible in the rows>. Cost: <live price>, if applying this change affects future billed matches. Want me to use that, or adjust it? ``` For other signals use `Target / Signal / Detail / When`, limited to returned fields. Never replace the table with a count or a role roll-up. When the user asked to calibrate, a recommendation to leave the filter broad still ends with the same yes-or-adjust prompt; it gives them the smallest useful control. Read monitor state and safely attributed rows at about 30, 60, and 90 seconds; this is the live view, not a new `tail` command. Show a small result table as soon as matches arrive. The initial forward scout ends at 90 seconds. - At 30 and 60 seconds, report waiting only when useful. - At 90 seconds with no row or provider error, return **no sample yet** and leave the forward monitor active. This is not a filter conclusion. Offer the next 30-day historical step with its live price and one approval question; do not create, replace, or widen anything before consent. - A historical step has its own provider completion window. Do not call a 30- or 60-day step empty, replace it, or offer the next rung until that documented window has passed (Deepline Native can deliver matching findings during its first 24 hours). Leave the current historical monitor intact while it is pending. - Only after that completed 30- or 60-day historical step is empty may you offer the next rung (60 or 90 days) with a new live price and approval. Stop at 90 days. - When rows arrive, keep matching patterns and remove only an observed off-target pattern. Verify the stored update, then observe it forward. An update does not request new historical matches. At 90 seconds with no row, say only that no sample has arrived, the watcher is still on, and offer the next priced historical step. A provider/contract failure is **Blocked**, not an empty result. A rejected signal is **Stopped**: confirm future matches stopped and existing rows remain. Read back the stored definition internally after deployment. In post-deployment updates, mention a destination or downstream Play only when it helps the user act; before approval, always disclose it. For a requested filter change, use the same tight loop: ```bash deepline monitors update stripe-cfo-job-openings \ '{"payload":{"job_titles":"\"Chief Financial Officer\" OR \"VP Finance\""}}' --json deepline monitors get stripe-cfo-job-openings --json ``` When managing a fleet, prove this loop on one monitor first. Keep workers bounded and save each requested patch/read-back result. A `provider_monitor_control_state_conflict` (HTTP 409) means another writer changed the monitor first: read it again and retry only if needed. Never treat a 409 as success or retry it blindly. ## Find monitor types and read their filters Monitor types live on the `tools` surface, alongside every other capability. Browse them, then read one type's exact filters + stream columns: ```bash # Browse the monitor types you can deploy deepline tools list --categories monitors deepline tools search "company radar" # Read one specific monitor variant's full contract deepline tools get deepline_native.company_job_openings ``` (`deepline monitors available [tool-id]` is a legacy alias for the same discovery — prefer `tools`.) The contract for a type gives you everything you need to deploy and to filter: - **payload_schema** — the deploy-time filters you set in the monitor `payload` (typed: required fields, allowed values). - **stream columns** — the row fields a play filters on with `sqlListeners.where` (the post-ingestion filter surface). - **pricing** — live Deepline price, charge timing, and pricing basis. - **a deploy example** — `deepline monitors deploy '<def>'`. A monitor type is deployed, not executed: `deepline tools execute <monitor-type>` is rejected and points you at `deepline monitors deploy`. ## Command set All commands accept `--json` (also automatic when stdout is piped). | Command | What it does | | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deepline monitors status` | Report whether you have monitor access (`has_access`). **Run first.** | | `deepline monitors health [--fleet <id>]` | Read the whole-scope delivery and convergence snapshot: desired versus provider-observed state, deactivation reasons, and monitors silent inside the window. A first result may have `refresh.state: "running"`; poll the same read until it is `"completed"` before treating its counts as whole-fleet facts. It currently reports the billing cause but does not yet offer an org-wide recovery action. | | `deepline monitors audit [--fleet <id>]` | Re-read the upstream resources already bound to Deepline and record the observations. Read-only and idempotent. Use this to establish provider evidence, not to resume billing-paused ingestion. `--watch` waits for the scope to settle. | | `deepline monitors repair [--fleet <id>]` | Queue only provider-observed versus desired-state disagreements onto the ordinary convergence loop. It does not create a missing billing-recovery decision; run `audit` first and use `--dry-run` before asking it to change anything. | | `deepline tools list --categories monitors` / `tools get <tool-id>` | **Preferred** discovery. Browse the monitor types you can deploy, and read one type's payload schema + stream columns + pricing. See "Find monitor types and read their filters". | | `deepline monitors available [tool-id]` | Legacy alias of the `tools` discovery above (still works). Read-only; `--full` or a tool id for one type's full contract. | | `deepline monitors check '<definition>'` | Validate a monitor definition without deploying. Read-only; spends nothing. Also accepts `--file <path>` or `--file -` (stdin). | | `deepline monitors deploy '<definition>'` | Deploy a monitor (positional JSON, `--file <path>`, or `--file -`). Mutates workspace state and may spend Deepline credits. `--dry-run` shows the preflight (validity, deploy cost in Deepline credits, existing monitors that may already cover the scope) without deploying. | | `deepline monitors list` | List the monitors you have deployed. `--status active\|inactive\|all` (default `active`), `--limit`, and `--cursor` page a bounded result. Read `total`, `returned`, `has_more`, and `next_cursor`; keep paging while `has_more` is true. `--include-consumers` adds downstream consumer health but is intentionally limited to 20 rows. | | `deepline monitors get <key>` | Show one deployed monitor by its public key. Read-only. When `monitor_spec.available` is true, `monitor_spec.fields` lists every deployable payload field with its description and constraints; legacy records may report it unavailable. | | `deepline monitors test <key> '<payload>'` | Validate an explicit provider-shaped callback against the deployed binding without persistence or dispatch. It is the safe event-shape diagnostic; it does not prove the upstream provider will emit a future event. | | `deepline monitors validate <key>` | Ask the deployed monitor's provider configuration to validate. This is a configuration check, not delivery evidence. | | `deepline monitors update <key> '<patch>'` | Patch a deployed monitor (`<patch>` is a JSON object; `--file` also works). Read the definition and run `check` on the merged shape first; an update can replace an upstream resource. | | `deepline monitors delete <key>` | Delete a deployed monitor and its upstream resource. Prompts y/N in a terminal; non-interactive runs must pass `--yes`. `--dry-run` previews the preflight. | | `deepline monitors reactivate <key>` | Reactivate a previously disabled deployed monitor. May spend Deepline credits; `--dry-run` shows the cost first. | | `deepline monitors fleets init` / `sync` / `get` | Create a Fleet definition, converge it, or inspect its computed status and drift. `sync --dry-run` plans; `sync --wait` follows the durable Fleet operation. | | `deepline monitors fleets deactivate <fleet-id>` | Stop every monitor owned by one Fleet. It is destructive, supports `--dry-run`, requires `--yes` non-interactively, and can `--wait`. | | `deepline monitors fleets reactivate <fleet-id>` | Explicitly recreate a Fleet after credit-triggered suspension. It may restart lifecycle billing; use `--yes --wait` after a dry-run. A credit top-up alone does not currently recreate deleted upstream radars. | ## Billing pause: current safe operator path `status: active` is control-plane intent, not delivery proof; start with `health` when a feed is quiet. `audit` records provider evidence and `repair` converges existing intent. Neither resumes a zero-credit pause. There is no org-wide direct-monitor resume. After credits or a cap are fixed: 1. Read `deepline monitors health --json` until `refresh.state` is `"completed"`. 2. Reactivate a credit-suspended Fleet through its retained owner: ```bash deepline monitors fleets reactivate <fleet-id> --yes --wait --json ``` 3. For a fully disabled direct monitor, read its cost before recovery: ```bash deepline monitors reactivate <key> --dry-run --json deepline monitors reactivate <key> --json ``` Do not fan out direct recovery while suspension is still stopping resources: it can race delete/create. Report that gap rather than inventing bulk recovery. ## Recover from errors by code Read the returned state before retrying. For validation errors, correct every reported field against the live contract and rerun `check`. For insufficient credits, report the required credits, balance, and shortfall, then stop. Retry a transient read or check once; do not blindly repeat a create, settlement, or cleanup failure. ## Operate safely Use `check` for every definition and `deploy --dry-run` before deployment, reactivation, or deletion. An update has no dry-run: read the full definition, validate the merged result with `check`, update only the requested patch, then read it back. An update keeps the public key and existing rows, but may replace the upstream resource. It does not promise a backfill. Before a paid deployment, list the whole registry with `--status all`. Follow `next_cursor` while `has_more` is true. Reuse a monitor with the same tool and scope; reactivate a disabled match instead of creating a duplicate. Monitors write to shared streams, so inspect the stream and known dependent Plays before changing scope. `sqlListeners.where` can narrow a Play's reaction, but cannot prevent a monitor from accepting a billable event. For a fleet, prove the approved minimally filtered scout on a bounded subset first, then use bounded concurrency and preserve each deploy/read-back result. A 409 means another writer changed the monitor: read it again before deciding whether a retry is needed. For a provider rate limit, return its wait to the user; never automatically retry a create. If a pilot is empty, confirm the stored definition, active state, output stream, and Play filter. A zero-result sample is inconclusive. Keep the monitor active or run one approved, priced diagnostic; do not widen several filters or call the signal broken. `check` validates a definition, not provider coverage. Only report live Deepline credit terms. The contract can price deployment, reactivation, accepted events, or recurring renewal. For event-priced monitors, future total is unknown without measured volume. Never expose provider cost. ## Update and downstream automation When the user changes a monitor, report the requested change, the active scope, known downstream Plays, and live Deepline pricing. Do not claim an arbitrary title expression is broader or narrower. A disabled monitor stores updates but does not contact the provider until reactivated. A monitor writes event rows; a Play can react to each row with a `sqlListeners` trigger for the tool and stream shown by `tools get`. Use a `where` clause only when the stream schema documents the field. Validate and publish the Play before reactivating a monitor when that Play must be ready for new events. The SDK uses the same definition and lifecycle contract; see [`../references/monitor-sdk.md`](../references/monitor-sdk.md). ## When to reach for a monitor - Continuously capturing an event feed: reply-received events on a campaign, new job postings for a company set, funding/intent signals for target accounts. - The value is the _ongoing stream_, not a one-time pull. For a one-time pull, use a normal enrichment/sourcing tool or play instead. - You want a play to fire the moment a provider event lands (bind a play's `sqlListeners` trigger to the monitor's table). ## Monitor definition shape A definition is a single JSON object: ```json { "key": "company-job-openings", "tool": "deepline_native.company_radar", "name": "Company job openings", "payload": { "domain": "stripe.com", "radar_type": "company_job_openings" }, "controls": {} } ``` - `key` — public monitor instance id (you reference it in `get`/`update`/`delete`). - `tool` — a live Deepline-native tool id. Get the valid ids and each `payload_schema` from `deepline tools list --categories monitors` / `deepline tools get <tool-id>`. - `payload` — tool-specific; must match that tool's `payload_schema`. - `name` — optional human label. `controls` — optional Deepline lifecycle metadata. The same object is what `defineMonitor({ ... })` returns (typed) and what `client.monitors.check`/`deploy` accept — the CLI JSON and the SDK definition are one shape. See "Monitors as code (SDK)". ### Priority radar exception Use `controls.execution_type: "priority"` only for an urgent preview or calibration. Deepline adds the provider-facing marker; do not put `custom_fields` in `payload`. Regular and bulk creation stay normal. An org has ten active-or-in-flight priority slots. At the cap, report it and get the user's choice; never delete a customer radar automatically. To retain a radar but release its slot, patch `{"controls":{"execution_type":null}}`. ```json { "key": "stripe-job-openings-preview", "tool": "deepline_native.company_radar", "payload": { "domain": "stripe.com", "radar_type": "company_job_openings" }, "controls": { "execution_type": "priority" } } ``` -
deepline-plays.md 21.7 KB
--- name: deepline-plays description: 'Create, audit, and modernize Deepline Plays that combine tools or other Plays, with durable datasets, fallback logic, joins, projections, and custom run/export behavior.' --- # Deepline Plays Recipe Use this recipe when the user needs a custom Deepline Play or asks to audit an existing local or saved Play for deprecated APIs, tool IDs, Play references, or command patterns. Read budget: normal tasks should use this recipe plus at most one plays reference. If you need more than one reference, name why before loading it. ## Negative Gates | If the task is... | Use instead | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | A single existing prebuilt exactly solves the request | `deepline plays search` -> `deepline plays describe` -> direct `deepline plays run` | | Ordinary row enrichment, waterfall columns, CSV processing, or per-row research | `enriching-and-researching.md` (prebuilt plays and their batch forms) | | Company/contact/TAM sourcing strategy | `finding-companies-and-contacts.md` and matching GTM recipe | | Persisted webhook/cron-style automation, orchestration, or fanout | Stay in this recipe and author a custom play with explicit inputs, idempotency, and run/export behavior | | Exact SDK or HTTP syntax is the only question | Load the generated reference named in Exact Syntax Escrow below | ## Core Loop 1. **Preflight:** when spend or cloud execution is likely, run `deepline preflight --json` as one standalone command and wait for it before launching any parallel Deepline commands. 2. **Describe before spend:** for plays, `plays search` -> `plays describe`; for tools, `tools search` -> `tools describe`. 3. **Choose direct vs compose:** direct-run only when the described contract exactly matches input, output, export, freshness, and pricing. Otherwise bootstrap, wrap, or author a custom play. 4. **Check before run:** `plays describe` gates prebuilts; `plays check <file>` is mandatory for local, bootstrapped, or forked plays. 5. **Pilot before scale:** run 1-3 rows or a small sample, then inspect/export. 6. **Report reality:** run id, export path, charged Deepline credits or why not visible, executed/reused/failed counts when available, and repair class. Safe planning-only commands: auth/health/balance, `plays search`, `plays describe`, `tools search`, `tools describe`, `plays check`, `plays bootstrap --help`, and local scaffolding. Do not call `plays run` or provider execution in planning-only mode. ## Audit Existing Plays Treat “audit my Plays,” “check whether these Plays are outdated,” and “find deprecated Deepline usage” as a read-only check unless the user also asks for repairs. `deepline plays check` owns the authoring and deprecation policy; do not recreate a second checklist from memory. 1. Establish whether the scope is local `*.play.ts` files, saved non-archived workspace Plays, or both. Inventory local Plays with hidden and gitignored files included while excluding dependency/build directories. Inventory saved Plays with `deepline plays list --json`; do not claim completeness if the owned result page is capped or the user requested archived Plays that the CLI cannot enumerate. 2. For saved Plays, check every applicable live and dirty working revision. Fetch each revision's complete source bundle into a unique temporary directory outside the repository, preserve its entry file, and remove the exact temporary directory after the audit. 3. Run `deepline plays check <entry-file> --json` for every in-scope local or materialized saved revision. Checking is read-only and spends no provider credits. A named check is not a substitute for checking the revision's source bundle. 4. Read `issues` as the remediation queue. Fix every `error` first because it is Blocking; then fix every `warning` because it is non-blocking migration debt. Use each issue's `path`, `hint`, `validOptions`, and `docsHint`; do not guess replacements. Rerun `plays check` after each Play until both arrays are empty. 5. If a warning says validation is partial—for example, a dynamic tool id could not be resolved—make the identity static or report that the Play could not be certified clean. A successful check with unresolved warning issues is not a clean deprecation audit. Report one row per emitted issue with the Play/revision, severity, path, message, and prescribed fix. Finish with error and warning counts and name every checked Play/revision. Do not edit during an audit-only request, and do not publish or run repaired Plays without separate authorization. `plays check` audits Play source, not neighboring shell scripts or runbooks. If the user explicitly includes companion automation in scope, audit those separately for retired CLI commands and label that result separately from the Play linter. ### Trigger notification handoff After publishing a cron- or webhook-triggered play, verify the product notification path. Do not assume the trigger can report its own failure. ```bash deepline notifications list deepline notifications events deepline notifications slack channels --search pipeline deepline notifications add pipeline-watchdog --to slack:#pipeline-alerts --for play.cron.failed deepline notifications test pipeline-watchdog ``` Slack OAuth belongs in Dashboard → Integrations. This CLI only configures named notifications: each one selects a connected provider target and the Play events it receives. Use `deepline notifications list` before editing a rule; do not guess event IDs. Delivery retries and dead-letter handling are bounded internal reliability behavior, not a customer configuration surface. ## Which Path | Situation | First commands | Gate | | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | Existing play may fit exactly | `deepline plays search "<job words>" --json`, then `deepline plays describe prebuilt/<name> --json` | Input/output/export/pricing/freshness match | | CSV needs aliases, validation, projection, or joins | inspect headers, describe candidate play, then `plays bootstrap` or author wrapper | `plays check` and pilot pass | | Custom multi-tool or multi-play orchestration | search/describe each tool/play contract, then author a `.play.ts` | stable ids, durable datasets, and explicit final projection | | Webhook/cron-style automation or cloud workflow replacement | author a custom play with explicit inputs, idempotency, and run/export behavior | `plays check`, small pilot, and clear trigger handoff | | Company -> contacts -> email/phone fanout | use GTM sourcing docs first, then compose plays/tools only after the account/contact grain is clear | pilot proves account grain and contact identity | | Billing, rerun, export, cached rows, failed rows, suspicious output | `runs get`, `runs export`, `runs logs` | no paid rerun until run metadata is understood | | Existing Play may use deprecated APIs, tool IDs, or aliases | inventory, `plays check`, then describe every referenced tool/Play | report line-level findings before any edit, publish, or run | Names in docs are hints. Live `search` and `describe` are the source of truth: ```bash deepline plays search "<job words>" --json deepline plays describe prebuilt/<candidate> --json deepline tools search "<provider need>" --categories <category> --json deepline tools describe <tool-id> --json ``` ## Direct Prebuilt Run Direct-run only when exact: - described scalar/CSV/API input matches the user input - no CSV mapping or semantic repair is needed - output schema includes the requested result - export dataset path is known - freshness/caching behavior is acceptable - pricing mode and likely scale are acceptable Typical flow: ```bash deepline plays describe prebuilt/<name> --json deepline plays run prebuilt/<name> --input '{"field":"value"}' --watch deepline runs get <run-id> --full --json deepline runs export <run-id> --dataset result.rows --out rows.csv ``` For CSV prebuilts, compare required headers to actual headers. If aliases are unsupported or output projection is custom, bootstrap a wrapper instead of editing the prebuilt. ## Bootstrap, Wrap, Fork Bootstrap is the composition tool. It is not anti-prebuilt. Bootstrap or wrap when: - CSV headers need mapping, validation, or projection - a prebuilt is a useful stage but not the whole answer - company rows need people/contact/channel fanout - provider source rows need durable row state - final output needs flat user-facing columns - row gates, fallback legs, miss reasons, or stale policy matter Fork only when internals need to change: provider/tool order, internal stale policy, getter metadata, billing stage, or native prebuilt logic. Do not fork for simple CSV aliases or final formatting. ```bash deepline plays bootstrap <family> --from <source> --using play:prebuilt/<candidate> --limit 5 --out workflow.play.ts deepline plays get prebuilt/<name> --source --out fork.play.ts deepline plays check workflow.play.ts ``` Route families: `people-list`, `company-list`, `people-email`, `people-phone`, `company-people`, `company-people-email`, `company-people-phone`. If bootstrap syntax fails, run `deepline plays bootstrap --help` or route help and retry with explicit stage flags such as `--people`, `--email`, or `--phone`. ## Authoring Basics Use the current V2 shape from generated references when exact syntax matters: ```ts import { definePlay } from 'deepline'; type Input = { limit?: number }; export default definePlay( 'gtm-play', async (ctx, input: Input = {}) => { return { ok: true, limit: input.limit ?? 5 }; }, { billing: { maxCreditsPerRun: 50 } }, ); ``` Authoring rules: - Prefer typed inline input. Import validators only if generated refs or bootstrap output prove they exist. - Use `ctx.csv`, `ctx.dataset`, `ctx.tools.execute`, `ctx.runPlay`, `ctx.step`, `ctx.fetch`, and `ctx.secrets`. - Do not use local `fs`, raw `fetch`, or shell commands anywhere in a Play, including inside `ctx.step`; route external I/O through the matching `ctx.*` API so retries remain safe. Put local nondeterministic computation such as `Date.now` or `Math.random` in a durable `ctx.step` callback. Always use `ctx.secrets` for credentials, never `process.env`, including inside a step — see External HTTP And Secrets below. - Use stable ids for paid work. Rename ids only to refresh wrong/stale provider data or changed semantics. - The default Play runtime is 30 minutes. For a bounded long batch, set the Play-level option `runtime: { timeout: '90m', size: 'standard' }`; static whole-minute/hour durations are supported up to `4h`. This is not the CLI wait timeout or `ctx.tools.execute({ timeoutMs })`, which applies to one provider-call transport. Preserve row-level state and split work when a batch is unbounded. - Prefer one paid operation per dataset cell. Put shaping, projection, `status`, `miss_reason`, display fields, and transformations in separate pure columns after the paid column. - For recurring sourcing, use `.run({ key: 'domain', mode: 'net_new' })` on the candidate table. It atomically returns only previously unseen domains; ordinary `upsert` reruns return known rows too. This cannot suppress rows at a provider before that provider returns them. - Return datasets for CSV/exportable outputs. - Use declared getters. Do not parse raw payload paths when `extractedValues.*.get()` or `extractedLists.*.get()` exists. - For query tools such as `query_customer_db` and `snowflake_run_query`, use `result.extractedLists.rows.get()` and return that Dataset Handle for full-row export. Do not build exports from a raw response preview in newly authored Plays. - Dataset Handles are async-only, regardless of whether rows are already in memory. Use `await rows.count()`, `await rows.first()`, `await rows.at(index)`, `await rows.peek(limit)`, `await rows.materialize(limit)`, or `for await...of`. Do not use `.length`, numeric indexing, spread, or synchronous `for...of`. - Project to flat user-facing columns with `status`, `miss_reason`, evidence/source, and requested output fields. ### Cron input is explicit and revision-pinned A cron trigger receives `{}` unless its binding declares `input`. Put every scheduled argument in the static JSON object below; do not rely on a handler default for a side effect such as `apply`. The object is pinned with the published revision, so retries retain the event revision's arguments after a later republish. ```json { "cron": { "schedule": "0 8 * * 1-5", "timezone": "America/New_York", "input": { "apply": true, "batchSize": 100 } } } ``` `input` must be an inline JSON object—no variables, spreads, or functions. The most common cell: a tool call. Column resolvers are positional `(row, rowCtx)`; call `rowCtx.tools.execute({ id, tool, input, description })` (all four required; `id` is the durable receipt key) and read the envelope — `result.status` or declared getters. For a newly admitted raw-v2 Play, read `result.toolResponse.rawV2` only when the full scrubbed provider response is genuinely required and no declared getter represents the needed field. A stored legacy artifact can expose only `toolResponse.raw`; keep that access until its response contract is deliberately migrated: ```ts /** @mermaid probe-accounts * flowchart TD * accounts[("Account rows")] --> loop * subgraph loop["For each account"] * probe["Probe the domain"] --> flag["Flag success"] * end * loop --> out["Return the probed accounts"] */ import { definePlay } from 'deepline'; export default definePlay( 'probe-accounts', async (ctx, input: { rows: Array<{ domain: string }> }) => { // @mermaid-node accounts type:"dataset" out:"accounts" const accounts = await ctx .dataset('accounts', input.rows) // @mermaid-node probe out:"probe_status" .withColumn('probe_status', async (row, rowCtx) => { const result = await rowCtx.tools.execute({ id: 'probe', tool: 'test_rate_limit', input: { key: row.domain }, description: 'Probe the account domain.', }); return result.status; }) // @mermaid-node flag out:"probed" .withColumn('probed', (row) => row.probe_status === 'completed') .run({ key: 'domain' }); // @mermaid-node out out:"$output" return { accounts }; }, { description: 'Probe each account domain and flag success.' }, ); ``` ### External HTTP And Secrets A play reaches a non-Deepline API with `ctx.fetch`, and authenticates it with `ctx.secrets`. `await ctx.secrets.get("NAME")` resolves an allowed secret only inside the executing Play. Treat it like a Vercel environment variable: do not log or return it. Pass credentials through `ctx.secrets.bearer(...)` or `ctx.secrets.header(...)` so the request requires HTTPS and its credential is redacted from Deepline fetch receipts. ```ts import { definePlay } from 'deepline'; export default definePlay( 'campaign-name-sync', async (ctx) => { const apiKey = await ctx.secrets.get('INSTANTLY_API_KEY'); const res = await ctx.fetch( 'list-campaigns', 'https://api.instantly.ai/api/v2/campaigns', { auth: ctx.secrets.bearer(apiKey) }, ); if (!res.ok) { throw new Error(`Instantly returned ${res.status}: ${res.bodyText}`); } const body = res.json as { items?: Array<{ name: string }> } | null; return { names: (body?.items ?? []).map((item) => item.name) }; }, { description: 'Read campaign names from Instantly with a workspace secret.', }, ); ``` **Durable call-site keys must be static string literals.** The `ctx.fetch` key, the `ctx.dataset` key, and the `ctx.step` id name durable call sites, so check, publish, and replay have to agree on them before the body runs. A computed key fails check with `ctx.fetch key must be a non-empty static string. The value could not be resolved statically.` When a top-level `ctx.step` result depends on input or query values, retain a static step id and pass the stable semantic identity separately, for example `ctx.step('generated-at', async () => Date.now(), { semanticKey: input.id })`. The callback must remain an inline arrow or function expression. Row-scoped steps already include dataset row identity; add a semantic key only when it further identifies the work. Plan around it before you write the play, because it decides the shape: - A play **cannot page a large table**. `for (const p of pages) ctx.fetch(key(p), ...)` does not compile, and unrolling 149 literal keys is not a design. - Instead **push the aggregation server-side and call it once** — a SQL function, a view, or a provider endpoint that returns the whole result. This is usually the better architecture anyway: one durable call, one receipt. - To fan out over rows, use `ctx.dataset` with a static key. Per-row receipt identity comes from the row, not from the key — that is the supported way to do N-of-something. Three more things that cost people iterations: - `res.json` is a **property**, not a method. The body is read once at request time so the call can be checkpointed and replayed, so `await res.json()` is a type error. It is `null` both when the body is empty and when it is not valid JSON, so check `res.ok` and fall back to `res.bodyText`. - `init.auth` accepts one credentialed header or an array of headers. Each array entry must target a distinct header. - Manage stored values with `deepline secrets set` / `deepline secrets list`. Names are uppercased. Declare them in the Play's top-level `secrets` option when the play needs them present at publish time. ### The `@mermaid` block is the play's UI The block is what the dashboard draws for this play. "Change how the play looks" = edit the block, `deepline plays check`, republish. Rules, using the example above: 1. **One block per export.** Forked prebuilts carry two: `/** @mermaid scalar */` and `/** @mermaid batch */`. Every line is a node, edge, `subgraph`, or `class` — prose lines are errors. A `subgraph` wired to its dataset renders as the loop. ~12 boxes max, labels under 48 chars, no counts in labels. 2. **Shapes are cosmetic except two.** `{…}` = decision; `[[…]]` claims a `ctx.runPlay` (error otherwise). Datasets come from `type:"dataset"` on the annotation — required for every dataset you `.run()`. 3. **Bind with `// @mermaid-node <id> out:"<identifier>"`** above the statement. `out:` is code: the assigned const (`out:"result"`), the column name inside a loop, `out:"$output"` on returns. Words go in the box label, never in `out:` (`out:"raw companies"` is rejected). Also available: `in:"row.domain"`, `label:"…"`. 4. **A box no statement runs**: `class a,b sketch` (or `type:"conceptual"`). Never sketch a subgraph id. Undrawn computed columns go in `.run({ undrawnColumns: [...] })`. 5. **Decisions**: unique label on every branch edge (`ok -->|found| next` — unlabeled is an error), max 3 branches, `arm:"run"` on the `runIf` run side. 6. **`plays check` reviews all this — for docflow-gated accounts.** Ungated, the block isn't validated yet: author it to these rules anyway; they're exactly what check enforces. ### Provider fallthrough New Plays receive typed tool failures. Catch only `ProviderTransientError` when another read provider can answer the same question. Let validation, authentication, billing, Deepline, and unknown failures stop the Play. Keep the last provider call outside the catch so an exhausted waterfall fails loudly. ``` try { return await primary(); } catch (error) { if (!(error instanceof ProviderTransientError)) throw error; } return fallback(); ``` Do not branch on error messages or catch `ToolExecutionError` as a generic fallthrough signal. The generated SDK reference documents every stable field, the `retryable` distinction, and the explicit legacy-contract option. ## Exact Syntax Escrow Load these only when the task needs exact syntax or repair details: - `references/plays-run-export-inspect-repair.md`: before scale; after every meaningful run; for billing, rerun, export, cached rows, failed rows, logs, suspicious output, partial repair, or UI/run mismatch. - `references/plays-sdk-reference.md`: exact current SDK signatures for `.play.ts` authoring, `definePlay`, `ctx.dataset`, `ctx.runPlay`, `ctx.tools.execute`, staleness, and SDK client calls. - `references/plays-api-reference.md`: exact API/manual invocation, polling, streaming, stop, list, inspect/export, and artifact routes. ## Finish Shape When work ran, summarize: - route and play reference - run id - rows requested and returned - executed/reused/failed counts when visible - charged Deepline credits or why credits are missing/zero - export path and dataset path - miss/failure classes - next action: scale, rerun, repair, or stop When no paid run happened, say so explicitly and list the safe commands used. -
find-qualified-titles.md 10.6 KB
--- name: find-qualified-titles description: "Use when finding real role-holders at known company domains from an ICP, especially prompts like 'find all job titles at these companies', 'find qualified titles', 'find RevOps or marketing-ops buyers', or when exact title discovery should precede paid people search." --- # Find Qualified Titles (company_titles -> ICP filter -> contacts) Given a list of companies and an ICP described in plain English, find the people who hold the matching roles - starting from each company's **real title roster**, not keyword guesses. The title roster is free, the ICP match is one small LLM call per company, and you only spend on contacts at the very end. **Why exact-match (`title_lists`) is correct here - not a tradeoff.** Normally exact title matching is brittle (it misses "Sr Director Marketing Operations" if you typed "Senior Director..."). That problem does not exist in this pipeline, because the titles come from `company_titles` - they are the company's _own verbatim roster strings_. The LLM picks from that list, so every matched title is guaranteed to resolve. `title_lists` then returns **all exact matches for every title you selected** (it's an OR across the list, exact per entry): pick 5 titles -> get every person holding any of those 5. This is the whole payoff of qualifying against the real roster first. ## When to use - **Primary path** when the user asks for nuanced functions or real job titles at named companies, such as "AI leadership at Mount Sinai." - "Find all job titles at these companies" / "what roles exist at X". - "Find the marketing ops / RevOps / Salesforce buyers at these accounts." - "Find qualified titles" - user has accounts + an ICP and wants the real owners of a function. - Any time you would otherwise guess `title_filters` keyword patterns and miss real titles (e.g. "Revenue Architect" instead of "VP Sales", "GTM Systems" instead of "Sales Ops"). Do **not** reach for paid people-search (`peopledatalabs_person_search`, `crustdata_v3_person_search`) for this. `company_titles` answers "what titles exist here" for free; people-search is for when you already know the persona and need volume. ## Inputs - A CSV (or just a list) of company domains. `domain` column required; `company_name` / `company_linkedin` optional. - An ICP in plain English (the qualification criteria). ## Quick reference | Step | What | Tool | Cost | | ---- | ---------------------------------------------- | ------------------------------------------------ | ----------------------- | | 1 | Full title roster per company | `company_titles` | FREE | | 1b | Flatten nested titles -> scalar column | `run_javascript` | 0 | | 2 | LLM filters roster to ICP-matching titles | `deeplineagent` | cheap (1 small call/co) | | 2b | Flatten `matched_titles` -> scalar column | `run_javascript` | 0 | | 3 | Find all holders of the matched titles (exact) | `deepline_native_search_contact` (`title_lists`) | LinkedIn-only tier | | 4 | (optional) reveal email / phone | `enrich_contact` / `enrich_phone` | only on kept rows | ## Why this shape (non-obvious rules, all verified live) - **`title_lists`, not `title_filters`.** Matched titles are exact strings from the company's own roster - `title_lists` does exact, full-string matching and returns every holder of each title you pass. `title_filters` is boolean keyword/substring matching (e.g. `"operations"` also catches "People Operations Intern") - use that only when you did NOT qualify against the real roster first and want a broad keyword sweep. - **`title_lists` works on `search_contact`, NOT `prospector`.** Verified live: `prospector` returns `422 unexpected key 'title_lists'` - its Deepline schema only accepts `title_filter`/`title_filters` + `limit`. (Upstream Waterfall once announced `title_lists` on Prospector, but Deepline's prospector parser does not wire it through; `search_contact` is the path that works today.) - **`page_size` caps results per call - raise it for big lists.** `search_contact` defaults to a small page. If you select many titles or expect many holders, set `page_size` high enough (or paginate with `page_number`) so matches aren't silently truncated. Don't assume 10 is enough when you passed 30 titles. - **Materialize nested output into flat columns before referencing it.** In a play, row cells, `company_titles` currently appears under `row.titles.output.titles` and `deeplineagent` structured JSON under `row.icp_match.result.object.<field>` or `row.icp_match.extracted_json.<field>`. A bare placeholder like `{{titles.output.titles}}` does NOT resolve, and a raw array placeholder breaks a JSON payload spec. Extract with `run_javascript` first. Direct `deepline tools execute --json` uses the V2 envelope (`toolResponse.raw...`), so do not copy direct execute paths into row-level JS without inspecting the persisted row. - Inside `run_javascript`, use the persisted row shape: `row.titles.output.titles` and `row.icp_match.result.object.matched_titles` / `row.icp_match.extracted_json.matched_titles`. - `title_lists[].titles` must be a real array at execution time, not a CSV string that looks like JSON. If a CSV-sourced cell still reaches `search_contact` as a string like `["VP Sales"]`, Deepline rejects it with a pre-provider `422 value must be an array`; that should not bill. Add a `run_javascript` parse/materialize pass immediately before `search_contact` when needed. - When injecting a live array-valued cell into a later payload, **quote the placeholder**: `"titles": "{{matched_titles}}"` (the interpolator substitutes the real array). ## Pipeline This is a five-column custom play over `companies.csv` — author it once per [deepline-plays.md](deepline-plays.md) with these columns in order (single-company probes: run the same tool via `deepline tools execute`): 1. `titles` — `company_titles` with `{"domain": row.domain}` (FREE roster). 2. `titles_flat` — `run_javascript`: `const t = row.titles?.output?.titles || row.titles?.result?.data?.output?.titles || []; return JSON.stringify(t);` 3. `icp_match` — `deeplineagent`, model `openai/gpt-5.4-mini`, prompt: `ICP: <describe the buying-power roles, e.g. Marketing Ops, Sales Ops, RevOps, Salesforce admin/architect; senior IC and above; exclude recruiters/finance/support/plain reps>. From this exact title list return ONLY matching titles as exact strings: ${row.titles_flat}. Return JSON.` with `jsonSchema {matched_titles: string[], reasoning: string}`. 4. `matched_titles` — `run_javascript`: `const match = row.icp_match || {}; const t = match?.extracted_json?.matched_titles || match?.result?.object?.matched_titles || match?.object?.matched_titles || []; return JSON.stringify(t.slice(0,100));` 5. `contacts` — `deepline_native_search_contact` with `{"domain": row.domain, "title_lists":[{"name":"icp","titles": <matched_titles array>}], "page_size": 50}`. Returns all exact matches per title (LinkedIn only — email/phone redacted); raise `page_size` when many titles matched. Contacts land at `contacts.output.persons[]` (legacy rows may use `contacts.result.data.output.persons[]`) with name, title, `linkedin_url`, seniority, and department. Flatten to one row per contact before any email or phone reveal: ```bash python3 .skills/deepline-gtm/scripts/flatten-search-contact-persons.py contacts.csv \ --contacts-col contacts > contact_rows.csv ``` If the skill is installed outside the repo, use the same script from the installed `deepline-gtm/scripts/` directory. This expansion is a file-level step, not another `run_javascript` column, because one company can produce many people. ## Tiered contact reveal - buy only what the user needs Step 3 (`search_contact`) returns LinkedIn only - the lowest upfront cost. **Ask the user which channels they actually need before spending**, then add only the steps they pick: | Tier | Tool | Returns | | ------------- | ---------------------------------------------------------------------------------- | -------------------------------------------- | | LinkedIn only | `search_contact` (above) | name, title, LinkedIn (email/phone redacted) | | + work email | `enrich_contact` on the kept `linkedin_url` (or `first_name`+`last_name`+`domain`) | + verified email | | + phone | `enrich_phone` on priority contacts only | + phone (top picks only) | Run email/phone only on the rows the user keeps; never blanket-enrich the full set. Cost note: if the user wants emails on _most_ contacts up front, `prospector` (boolean `title_filters`, returns contact+verified email in one call) can be cheaper net than `search_contact` + a separate `enrich_contact` per row - but `prospector` does **not** support exact `title_lists`, so you lose the roster-exact precision. Use `search_contact` when you want exact-title precision and LinkedIn-first; consider `prospector` only when broad boolean matching is acceptable and you want emails in one shot. ## Supplemental coverage after the roster path Run supplemental providers only after the roster-qualified `search_contact` pass: - `exa_people_search` can find public web/profile entities missing from contact databases. Verify the current employer and title before treating a result as a match. - `dropleads_search_people` can add database rows or contact data, but keep it supplemental for nuanced titles. Its keyword filters can miss a real roster title such as "Director, Mount Sinai AI Assurance Lab" when the guessed keywords do not occur in the title. For broad market sizing rather than named-company title qualification, `dropleads_get_lead_count` and `dropleads_search_people` remain appropriate primary tools. ## Notes - Don't pre-trim the roster before the LLM filter - let the LLM pick from the full list, then raise `page_size` on step 3 so all holders of the matched titles come back. - The LLM filter is only as good as the ICP prompt - be specific about include/exclude, and tell it to return **exact strings from the list** so titles map back to `title_lists`. - v2 SDK equivalent: `docs-examples/sdk-v2/companies-to-contacts-icp-titles.play.ts`. -
linkedin-url-lookup.md 8.4 KB
--- name: linkedin-url-lookup description: 'Resolve LinkedIn profile URLs from name + company with strict identity validation to avoid false positives.' --- # LinkedIn URL Lookup Find LinkedIn profile URLs when you have a name, with or without company context. ## When to use - "Find LinkedIn URLs for the contacts in my CSV" - "Resolve LinkedIn profiles from names and companies" - "I only have names — find their LinkedIn profiles" - "Verify these LinkedIn URLs match the right people" ## Execution 1. **Read [enriching-and-researching.md](../enriching-and-researching.md)** — the LinkedIn enrichment section covers provider selection and validation patterns. 2. **Read [finding-companies-and-contacts.md](../finding-companies-and-contacts.md)** — if you also need to find contacts first. ## Prebuilt first `prebuilt/person-to-linkedin-harvestapi` runs the maintained Serper candidate route with native HarvestAPI profile validation. Inspect the live contract before running it: ```bash deepline plays describe prebuilt/person-to-linkedin-harvestapi deepline plays run prebuilt/person-to-linkedin-harvestapi --input '{"first_name":"Jane","last_name":"Smith","company_name":"Acme"}' # CSV: deepline plays run prebuilt/person-to-linkedin-harvestapi-batch --input '{"csv":"contacts.csv"}' deepline runs export <run-id> --out contacts_with_linkedin.csv ``` The HarvestAPI play tries company-anchored Serper, name-only Serper, and Crustdata when an email is available. It validates the chosen candidate with `harvestapi_get_profile`, then scans later Serper results only when the first candidate fails validation. The older `prebuilt/person-to-linkedin` and `prebuilt/person-to-linkedin-batch` IDs keep their original Serper-validation behavior for existing workflows. Use the expanded manual sequence below only when you need a custom provider order. Pull the maintained play as a starting point, then inspect and check the fork before running it: ```bash deepline plays get prebuilt/person-to-linkedin-harvestapi --source --out ./fork.play.ts deepline plays check ./fork.play.ts ``` ## Expanded provider sequence for a custom fork Follow this order. Stop when you get a validated match. ### Step 1: Dropleads (free) Start with Dropleads — free people search that returns LinkedIn URLs directly. ```bash deepline tools execute dropleads_search_people --payload '{"filters":{"keywords":["Jane","Smith"],"jobTitles":["Sales"],"seniority":["VP","Director"]},"pagination":{"page":1,"limit":5}}' ``` For batch: _In a fork/custom play, this step is one `withColumn` calling the same tool per row._ ### Step 2: Serper Google search + HarvestAPI validation If Dropleads misses, search Google scoped to LinkedIn then validate the profile. **2a. Find candidate URLs with Serper:** ```bash # Name + company (highest confidence) deepline tools execute serper_google_search --payload '{"query":"\"Jane Smith\" \"Acme Corp\" site:linkedin.com/in","num":5}' # Name only deepline tools execute serper_google_search --payload '{"query":"\"Jane Smith\" site:linkedin.com/in","num":5}' # Name + title deepline tools execute serper_google_search --payload '{"query":"\"Jane Smith\" \"VP Sales\" site:linkedin.com/in","num":5}' ``` Parse the LinkedIn URL from `organic[0].link`. Skip results that aren't `linkedin.com/in/` URLs. **2b. Retrieve and name-validate:** ```bash deepline tools describe harvestapi_get_profile --schema-only deepline tools execute harvestapi_get_profile --payload '{"url":"https://linkedin.com/in/janesmith"}' --json ``` **Name-validate** the returned `element.firstName` and `element.lastName` against the source name (see Post-lookup name validation). Company/title are supporting signals only. If validation fails, try the next Serper result. If all Serper results fail validation, move to Step 3. For batch: ```bash deepline tools execute serper_google_search --input '{"query":"\"Jane Smith\" \"Acme\" site:linkedin.com/in","num":3}' deepline tools execute harvestapi_get_profile --input '{"url":"<top-hit-url>"}' --json ``` _In a fork/custom play these are two `withColumn` steps: search, then scrape + name-validate the top hit._ ### Step 3: Exa semantic search If Serper + validation fails, try Exa's semantic "find similar" approach. ```bash deepline tools execute exa_search --payload '{"query":"Jane Smith VP Sales at Acme Corp LinkedIn profile","numResults":3,"type":"neural","includeDomains":["linkedin.com"]}' ``` Exa is a weak fallback for name-only lookup (23% validated vs serper's 74% in a 253-person test). Still worth trying on serper misses - it recovered 3/36 failures. Name-validate the same way. ### Step 4: Crustdata (paid, ~1 credit) Structured people search with company domain context. _In a fork/custom play, this step is one `withColumn` calling the same tool per row._ ### Step 5: Prospeo (paid) Email + LinkedIn finder from name and company. ```bash deepline tools execute prospeo_enrich_person --payload '{"first_name":"Jane","last_name":"Smith","company_name":"Acme Corp"}' ``` Prospeo returns LinkedIn URLs alongside email when available. ## Scenarios ### Name only 1. Dropleads with whatever filters you have 2. Serper: `"Jane Smith" site:linkedin.com/in` → validate with HarvestAPI 3. Too many results? Add geography: `"Jane Smith" "New York" site:linkedin.com/in` 4. Exa neural search for the person 5. Still ambiguous? Ask the user for more info before spending credits ### Name + company 1. Dropleads with name + company 2. If miss, Serper: `"Jane Smith" "Acme Corp" site:linkedin.com/in` → validate with HarvestAPI 3. Exa: `"Jane Smith VP Sales Acme Corp LinkedIn"` 4. Crustdata people search with company domain ### Name only (event attendees, RSVP lists) When you have names but no company context, add event/role keywords to disambiguate: ```bash # OR-chain of likely titles improves serper relevance "\"Jane Smith\" (RevOps OR \"Sales Operations\" OR GTM OR Sales OR Growth) site:linkedin.com/in" ``` Use `run_javascript` to score serper results by GTM keyword density + geo before picking the best URL. Expect ~74% validated match rate on name-only with title keywords. ### Nickname handling Common variants: Mike/Michael, Bob/Robert, Bill/William, Liz/Elizabeth, Alex/Alexander/Oleksandr, Dan/Daniel, Sara/Sarah. - Serper handles this well: `("Mike" OR "Michael") "Smith" "Acme" site:linkedin.com/in` - For batch, expand CSV to include common variants before lookup ## Post-lookup name validation (mandatory) After scraping, compare profile name to source name. **Null out any URL where first+last don't match.** 26% of serper lookups returned wrong people in a 253-person test without this gate. Rules: - Last name: exact or substring (handles hyphenated, but not single-char abbreviations) - First name: exact, 3+ char prefix, nickname, or quoted nickname in profile (e.g., `Yerachmiel 'Rocky' Katz`) - Normalize accents (`Rodríguez`->`Rodriguez`) and strip punctuation/emoji before comparing Validation script and eval fixtures: ```bash python3 scripts/validate-linkedin-names.py --fixtures scripts/fixtures_name_validation.json # 52 test cases, thresholds: precision >= 0.95, recall >= 0.85 ``` ## Native HarvestAPI operations | Operation | Use | Starting input | | ------------------------------ | ---------------------------- | ---------------------------------------------------- | | `harvestapi_get_profile` | Profile lookup/validation | `url`, `publicIdentifier`, or `profileId` | | `harvestapi_get_profile_posts` | Posts published by a profile | `profile`, `profileId`, or `profilePublicIdentifier` | Confirm the live input and Deepline pricing with `deepline tools describe <operation>` before building a batch. Use Apify only when the native HarvestAPI provider does not expose the required result shape. ## Key rules - Prefer the maintained prebuilt unless you need a custom provider order. - In an expanded fork, Dropleads is free and structured; validate every URL it returns. - Serper candidates must be validated with `harvestapi_get_profile`. - Exa is a weak fallback (23% validated rate), but recovers some Serper misses. - Crustdata and Prospeo are paid fallbacks for a custom route. - **Name-validate every looked-up URL.** Company/title matching alone is not enough. - Pilot on `--rows 0` before the full batch. Row ranges are inclusive. - Extract the `/in/username` slug - strip query params and trailing slashes. - Without company context, add role keywords to serper query. -
portfolio-prospecting.md 3.7 KB
--- name: portfolio-prospecting description: 'Find companies backed by a specific investor or accelerator, then find contacts and build personalized outbound.' --- # Portfolio/VC Prospecting Find companies backed by a specific investor or accelerator (YC, a16z, Sequoia, etc.), then find contacts and build personalized outbound. ## Core insight: VC portfolio data is public Every major VC and accelerator publishes their portfolio online. **Do NOT waste turns trying to discover portfolio companies through Deepline search tools.** Instead, fetch the public portfolio page directly and extract company names from it. This is faster, cheaper, and more complete than any provider-based approach. ## What NOT to do Tested and failed: provider-side investor filtering on generic prospecting databases (irrelevant results), people-first then verify investor (~7-9% hit rate, wastes 60-80% of turns), Crustdata `crunchbase_investors` (inconsistent), `deeplineagent` per-row investor verification (~5-10s/row, unacceptable at scale) ## Proven approach **Step 1: Get the company list from the VC's public portfolio.** Common URLs: YC (`ycombinator.com/companies`), a16z (`a16z.com/portfolio`), Sequoia (`sequoiacap.com/our-companies`), Greylock/Benchmark (`/portfolio`). ```bash # Fetch YC companies page (or use parallel_extract if JS-rendered) curl -sS "https://www.ycombinator.com/companies" -H "Accept: text/html" -o $WORKDIR/yc_page.html deepline tools execute parallel_extract --payload '{"urls":["https://www.ycombinator.com/companies?batch=W26"],"objective":"Extract all company names, website domains, and one-line descriptions from this YC batch directory page","full_content":true}' ``` **Step 2: Filter to companies hiring your target role (optional).** ```bash deepline tools execute exa_search --input '{"query":"GTM Engineer site:ycombinator.com","numResults":50,"type":"auto"}' ``` For per-company filtering across the whole list, add this as a column in a custom play ([deepline-plays.md](deepline-plays.md)). **Step 3: Find contacts at each company.** Use guidance in [enriching-and-researching.md](../enriching-and-researching.md) for this **Step 4: Find emails via waterfall.** Use guidance in [enriching-and-researching.md](../enriching-and-researching.md) for this **Step 5: Generate personalized email copy** with `deeplineagent` and `jsonSchema`. If the row still needs fresh web lookup, do that in the same `deeplineagent` step or in a separate research pass first. Pilot on rows `0:2`, then run the full batch. Use guidance in [writing-outreach.md](../writing-outreach.md) ## Common pitfalls | Pitfall | What happens | Fix | | ------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------- | | Trying to discover portfolio companies via Deepline tools | Wastes 60-80% of turn budget on company discovery | Fetch the public portfolio page directly | | Using old `json_mode` fields from retired local AI docs | New AI tools ignore that contract and structured output drifts or fails | Pass a `jsonSchema` object to `deeplineagent` | | Searching with strict titles at small startups | 0 results — person hasn't been hired yet | Remove title filter, get broader roles, pick best match | | Using Hunter as primary email finder for <50 person companies | 0/25 fill rate | Use LeadMagic first — better small-company coverage | -
small-business-prospecting.md 2.9 KB
# Small Business Prospecting Use this for local SMB discovery like dentists, plumbers, med spas, agencies, or nearby storefronts. 1. Start with `serper_google_maps_search` when you need fast recall, loose discovery, or broad geo coverage. 2. Use `openwebninja_localbusiness_search` when you want structured Google Maps business rows with phone, address, rating, website, and optional `extract_emails_and_contacts=true`. 3. If the target area is map-bounded, prefer `openwebninja_localbusiness_search_in_area` or `openwebninja_localbusiness_search_nearby`. 4. Use Enformion for US business-registry depth: `enformion_business_search` for structured business records (ownership, addresses, registrations), `enformion_person_search` / `enformion_contact_enrich` to reach the owner or principal behind a storefront, and `enformion_workplace_search` to connect people to businesses. Strongest when Maps gives you the storefront but you need the legal entity or the owner's direct contact. 5. If you need broader non-maps company sourcing, `forager_organization_search` can be a useful complement, but it is not the primary local-business tool. Default pattern: - Serper Maps for discovery and query tuning. - OpenWebNinja Local Business for the structured list you will enrich or export. - Enformion to resolve the owner/principal and legal entity behind the storefront. Contact-email recovery pattern: - Start with Maps identity and the business website/contact page when the row has a normal homepage. - Treat Facebook and Instagram profiles as optional candidate sources, not mandatory steps. Consider them when the row's only website is a social profile, the official site is missing/thin, or a pilot/ground-truth sample suggests contact emails are in Facebook About blocks, Instagram profile contact fields, bios, link-in-bio pages, or recent posts. - Search ScrapeCreators without over-constraining to a category if profile tools are not showing up in the first pass: ```bash deepline tools search "facebook profile email scrapecreators" --json deepline tools search "instagram profile bio email scrapecreators" --json deepline tools search scrapecreators --json ``` - If available, test `scrapecreators_facebook_profile`, `scrapecreators_facebook_profile_posts`, `scrapecreators_instagram_profile`, and `scrapecreators_instagram_user_posts` on a tiny sample before scaling. Fall back to Serper/Firecrawl/Apify or direct website extraction when no managed profile route fits. - Add audit columns such as `facebook_url`, `instagram_url`, `social_email`, `social_email_source`, `social_identity_evidence`, and `social_contact_confidence` instead of overwriting the canonical email directly. - Accept social profile contact data only when the profile identity matches at least two of business name, address, phone, website/menu/booking link, or Maps profile. Pilot first on one query and a small limit before scaling.
-
-
references
-
clay-action-mappings.md 39.1 KB
# Clay Action → Deepline Tool Mappings Every Clay action maps to a specific Deepline CLI tool or native play. Use actual tool IDs in every generated script — never generic descriptions. ## ⚠️ Tool Discovery Protocol — Read First **This mapping is a starting-point reference, not a guarantee.** Deepline adds new tools, native plays, and provider integrations continuously. The right mental model: **For every Clay action, the selection order is:** 1. **Native play first** — check if a native Deepline play covers the action (they're stable, multi-provider, and cost-optimized). Current native plays: `name-and-domain-to-email-waterfall`, `company-to-contact`, and `person-to-phone`. 2. **Search for a dedicated tool** — `deepline tools search "<intent>"` before hardcoding any individual provider tool. New tools are added regularly. Examples: `deepline tools search "qualify person ICP"`, `deepline tools search "octave"`, `deepline tools search "email verify"`, `deepline tools search "add leads campaign"`. 3. **Verify the tool exists** - `deepline tools describe <tool_id>`. If it errors, the tool doesn't exist yet - use the `deeplineagent` fallback from this doc. 4. **Use the mapping below as a fallback** — when no native play or dedicated tool exists. ```bash # Standard discovery pattern before writing any play column deepline tools search "<action intent>" # find current options deepline tools describe <candidate_tool_id> # verify it exists + see payload schema ``` **Why this matters:** Deepline may add a native `octave_qualify_person`, `instantly_send_email`, or other integration at any time. Searching first means your script gets the better tool automatically — instead of being locked into a `deeplineagent` approximation. --- ## Model Translation | Clay model | Deepline column params | Notes | | -------------------------------- | --------------------------------------- | ------------------------------------------------ | | `gpt-4.1`, `claude` (clay-argon) | `"model":"anthropic/claude-sonnet-4.6"` | Complex reasoning, larger structured outputs | | `gpt-4.1-mini` | `"model":"openai/gpt-5.4-mini"` | Clay's mid-tier model for claygent-style columns | | `gpt-4o-mini`, `gpt-5-mini` | `"model":"openai/gpt-5.4-mini"` | Fast classify/generate | | `gpt-5-nano` | `"model":"openai/gpt-5.4-mini"` | Cheapest tier approximation | --- ## Complete Action → Tool Mapping | Clay action key | Deepline tool / native play | Notes | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `find-lists-of-companies-with-mixrank-source` (source type) | **Pass 1**: `crustdata_v3_company_search` — filters by location, employee count, industry, funding, and investors. Returns company name, domain, LinkedIn URL, HQ, funding, and firmographic fields. **Pass 2** (optional): `prospeo_enrich_company` — adds `description`, `employee_count`, `industry`, `type`. See Company Source section below. | Use `crustdata_v3_company_search_autocomplete` first for canonical filter values | | `enrich-person-with-mixrank-v2` | `leadmagic_profile_search` → `crustdata_v3_person_enrich` waterfall | See Person Enrichment section | | `lookup-company-in-other-table` | `run_javascript` (local CSV join) | Export company table to CSV first | | `lookup-multiple-rows-in-other-table` | `run_javascript` (local CSV join) | Same pattern | | `chat-gpt-schema-mapper` | `deeplineagent`; use `jsonSchema` when you need structured extraction | Single-value classification | | `normalize-company-name` | `deeplineagent` or `run_javascript` | JS preferred for pure string ops | | `generate-email-permutations` | `run_javascript` | Pure compute, no provider | | `validate-email` (all instances) | `leadmagic_email_validation` (one final gate) | Skip per-step validation; validate once after waterfall | | `wiza-find-email` | `dropleads_email_finder` (waterfall step 1) | Part of native play | | `find-email-v2` (Hunter) | `hunter_email_finder` (waterfall step 2) | Part of native play | | `leadmagic-find-work-email` | `leadmagic_email_finder` (waterfall step 3) | Part of native play | | `findymail-find-work-email` | `dropleads_email_finder` (waterfall fallback) | Covered by native play | | `enrich-person` (PDL) | `peopledatalabs_enrich_contact` (waterfall step) | Covered by native play | | `dropcontact-enrich-person` | `dropleads_email_finder` (waterfall step) | Covered by native play | | **Entire email waterfall group** | `name-and-domain-to-email-waterfall` | One play replaces all 6 finders when you have a domain; include `linkedin_url` when available | | `use-ai` (no web, simple) | `deeplineagent` | Match model tier | | `use-ai` (no web, structured) | `deeplineagent` + `jsonSchema` | | | `use-ai` (claygent + web) | Pass 1: `exa_search` → Pass 2: `deeplineagent` | Always split research and synthesis | | `octave-qualify-person` | `deeplineagent`, ICP scoring prompt, `jsonSchema` | See Octave section | | `octave-enrich-person` | `exa_search` + `deeplineagent` | | | `octave-run-sequence-runner` | Pass 1: `deeplineagent` (signals) → Pass 2: `deeplineagent` (email) | Always 2 passes | | `social-posts-get-post-activity-posts-and-shares` | `harvestapi_get_profile_posts` | Run-as-button in Clay — omit unless user needs posts | | `score-your-data` (unconfigured) | `run_javascript` keyword scoring | See Scoring section | | `add-lead-to-campaign` (Smartlead) | `smartlead_add_leads_to_campaign` | | | `add-lead-to-campaign` (Instantly) | `instantly_add_contacts_to_campaign` | | | `exa_search` (Clay native) | `exa_search` | Direct equivalent | --- ## Person Enrichment (LinkedIn URL → profile data) ### `enrich-person-with-mixrank-v2` **Best single tool** — `leadmagic_profile_search`: ```ts .withColumn('person_profile', 'leadmagic_profile_search', { profile_url: "{{linkedin_url}}" }) ``` Key output paths: `.output.body.full_name`, `.output.body.work_experience[0].company_website`, `.output.body.company_website` **Richer fallback** — `crustdata_v3_person_enrich`: ```ts .withColumn('person_profile', 'crustdata_v3_person_enrich', { professional_network_profile_urls: ["{{linkedin_url}}"], fields: ["basic_profile", "experience"] }) ``` Key output paths: `.output.body.matches[0].person_data.basic_profile.name`, `.output.body.matches[0].person_data.contact.business_emails[0].email`, `.output.body.matches[0].person_data.experience.employment_details.current[0].company_website_domain` **Work history / posts** — native HarvestAPI: ```bash deepline tools execute harvestapi_get_profile --payload '{"url":"<linkedin_url>"}' --json deepline tools execute harvestapi_get_profile_posts --payload '{"profile":"<linkedin_url>","page":1}' --out linkedin-posts.csv ``` --- ## Company Table Lookup (Clay cross-table join) ### `lookup-company-in-other-table` / `lookup-multiple-rows-in-other-table` Export the linked Clay table to a local CSV first (`clay_fetch_records.sh` schema mode). Then join with `run_javascript`: ```javascript // $WORKDIR/join_company.js // Adjust field names to match your table's column aliases const fs = require('fs'); const rows = fs .readFileSync(process.env.COMPANY_CSV_PATH, 'utf8') .trim() .split('\n') .slice(1) .map((line) => JSON.parse(line)); // adjust for CSV format const joinKey = row['company_domain'] || row['domain']; // ← your join key column return rows.find((c) => c.domain === joinKey) || null; ``` ```ts .withColumn('company_data', 'run_javascript', { code: "@$WORKDIR/join_company.js" }) ``` --- ## Email Waterfall (6 Clay providers → 1 Deepline native play) The entire Clay waterfall group collapses to one native play. Pick based on available input: **Have LinkedIn URL + name + domain** (preferred — highest hit rate): ```ts .withColumn('work_email', 'name-and-domain-to-email-waterfall', { linkedin_url: "{{linkedin_url}}", first_name: "{{first_name}}", last_name: "{{last_name}}", domain: "{{company_domain}}" }) ``` Compiles to: `dropleads_email_finder → hunter_email_finder → leadmagic_email_finder → deepline_native_enrich_contact → crustdata_v3_person_enrich → peopledatalabs_enrich_contact` **Have name + company only**: ```ts .withColumn('work_email', 'name-and-domain-to-email-waterfall', { first_name: "{{first_name}}", last_name: "{{last_name}}", domain: "{{company_domain}}" }) ``` **Have first + last + domain only** (cost-efficient — tries pattern validation first): ```ts .withColumn('work_email', 'name-and-domain-to-email-waterfall', { first_name: "{{first_name}}", last_name: "{{last_name}}", domain: "{{domain}}" }) ``` Compiles to: `leadmagic_email_validation (first.last@, firstlast@, first_last@) → dropleads_email_finder → hunter_email_finder → leadmagic_email_finder → deepline_native_enrich_contact → peopledatalabs_enrich_contact` **Alternative individual providers** (use `deepline tools search "email"` to see current list): - `icypeas_email_search` — 700M+ profiles, strong LinkedIn coverage; useful as a step if native play misses - `dropleads_email_finder` — included in native plays; available standalone too - Run `deepline tools describe icypeas_email_search` to verify tool exists before using **Final validation gate** (replaces all per-step Clay `validate-email` calls): Default — `leadmagic_email_validation`: ```ts .withColumn('email_valid', 'leadmagic_email_validation', { email: "{{work_email}}" }) ``` LeadMagic returns four relevant statuses (as `.output.body.email_status`): | Status | Meaning | Bounce rate | Charge | | ----------------- | -------------------------------------------------- | ----------- | -------- | | `valid` | Verified deliverable | <1% | Yes | | `valid_catch_all` | Catch-all domain; engagement data confirms address | <5% | Yes | | `catch_all` | Domain accepts all; unverifiable | Unknown | **Free** | | `unknown` | Mail server no response | Unknown | **Free** | | `invalid` | Will bounce | ~100% | Yes | **Accept `valid`, `valid_catch_all`, AND `catch_all` as "found"** — all three are as reliable as what Clay reports. `valid_catch_all` is the highest-confidence version (LeadMagic has engagement signal data for the address). Do not accept `unknown`. Alternative — `zerobounce_validate` (more detailed sub_status, better for catch-all domains): ```ts .withColumn('email_valid', 'zerobounce_validate', { email: "{{work_email}}" }) ``` Check `.status` and `.sub_status`. Use `zerobounce_validate` for each email; Deepline disables ZeroBounce batch validation because the upstream batch endpoint currently returns 403 Access denied from Deepline egress. Alternative — `dropleads_email_verifier` (cheapest option): ```ts .withColumn('email_valid', 'dropleads_email_verifier', { email: "{{work_email}}" }) ``` Run `deepline tools search "email validation"` to see all current options. --- ## Email Permutations ### `generate-email-permutations` Pure `run_javascript` — no provider: ```javascript // $WORKDIR/email_permutations.js const first = (row['first_name'] || '').toLowerCase().replace(/[^a-z]/g, ''); const last = (row['last_name'] || '').toLowerCase().replace(/[^a-z]/g, ''); const domain = row['company_domain'] || ''; if (!first || !last || !domain) return null; const perms = [ `${first}.${last}@${domain}`, `${first}${last}@${domain}`, `${first}_${last}@${domain}`, `${first}@${domain}`, `${first[0]}${last}@${domain}`, `${first}${last[0]}@${domain}`, `${first[0]}.${last}@${domain}`, `${last}.${first}@${domain}`, ]; return { permutations: perms, comma_separated_list: perms.join(',') }; ``` ```ts .withColumn('email_permutations', 'run_javascript', { code: "@$WORKDIR/email_permutations.js" }) ``` Prefer `name-and-domain-to-email-waterfall` over a hand-built waterfall when you already have a clean company domain. --- ## Company Source — Replacing `find-lists-of-companies-with-mixrank-source` Clay's Mixrank source fetches a pre-built list from a configured Mixrank query. The Deepline equivalent is a two-pass Python script: **discover with `crustdata_v3_company_search`**, then **enrich with `prospeo_enrich_company`** for fields Clay gets from Mixrank (description, industry, size, type). ### Pass 1 — Generate company list (`crustdata_v3_company_search`) ```python import json, subprocess, csv # CrustData filter payload — translate from Clay's Mixrank source config # Check the Clay table config or ask the user for the original filter criteria payload = { "filters": { "op": "and", "conditions": [ {"field": "locations.headquarters", "type": "(.)", "value": "Los Angeles"}, {"field": "locations.country", "type": "=", "value": "USA"}, {"field": "taxonomy.categories", "type": "(.)", "value": "software"}, {"field": "basic_info.employee_count_range", "type": "in", "value": ["51-200", "201-500", "501-1000"]}, ], }, "limit": 100, } result = subprocess.run( ["deepline", "tools", "execute", "crustdata_v3_company_search", "--payload", json.dumps(payload), "--json"], capture_output=True, text=True ) response_json = json.loads(result.stdout) accounts = response_json.get("result", {}).get("data", []) # Fields vary by CrustData result shape; preserve company name, domain, # LinkedIn URL, HQ, employee count, funding, and category fields when present. ``` **CrustData output → Clay field mapping:** | CrustData field | Clay formula field | | -------------------------------- | ------------------------------------------------------- | | `company_name` / `name` | Name | | `domain` / `company_domain` | Domain | | `linkedin_url` | LinkedIn URL | | `hq_location` / `city` + `state` | Location | | `organization_country` | Country | | — | Size, Description, Primary Industry, Type (need Pass 2) | ### Pass 2 — Enrich missing fields (`prospeo_enrich_company`, optional) Only needed if downstream passes reference `Description`, `Primary Industry`, `Size`, or `Type`. ```python # For each company from Pass 1 that lacks description/industry: result = subprocess.run( ["deepline", "tools", "execute", "prospeo_enrich_company", "--payload", json.dumps({"website": domain}), "--json"], capture_output=True, text=True ) enriched = json.loads(result.stdout).get("result", {}).get("company", {}) # Fields: description, employee_count, employee_range (= Size), industry categories, company_type ``` ### Cost comparison | Clay | Deepline | | --------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Mixrank source — bundled in Clay subscription | CrustData company search + optional Prospeo enrichment; check live tool catalog for Deepline credits | | Returns all fields in one step | Two passes; Pass 2 optional if downstream uses only domain/name/linkedin | ### Key questions to ask the user before generating the script 1. What filters did the Clay Mixrank source use? (location, size, industry, tech stack) — visible in the Clay source config or ask the user 2. How many companies total? Use a narrow pilot or count-like query before paging. 3. Does the pipeline actually use `Description`, `Industry`, `Size`, `Type`? If not, skip Pass 2. --- ## Company Name Normalization ### `normalize-company-name` For LLM-quality normalization: ```ts .withColumn('normalized_company', 'deeplineagent', { model: "openai/gpt-5.4-mini", prompt: "Normalize this company name: {{company_raw}}. Strip legal suffixes (Inc, LLC, Corp, Ltd, Holdings, Group). Return title case. Return ONLY the name, nothing else." }) ``` For pure string normalization (cheaper): ```javascript // $WORKDIR/normalize_company.js const name = row['company_raw'] || ''; return name .replace( /\b(Inc\.?|LLC\.?|Corp\.?|Ltd\.?|Limited|Co\.?|Group|Holdings?)\b/gi, '', ) .trim(); ``` --- ## Classification ### `chat-gpt-schema-mapper` ```ts .withColumn('job_function', 'deeplineagent', { model: "openai/gpt-5.4-mini", prompt: "Classify this job title into a function label. Rules: all lowercase except BizOps/RevOps/GTM Ops, <4 words, describe the vertical. Return ONLY the label.\n\nJob title: {{job_title}}" }) ``` No `jsonSchema` needed for a single-value string output. --- ## AI Columns — No Web ### `use-ai` (useCase: `use-ai`, no web tools) ```ts // fast reasoning / generation .withColumn('data_warehouse_formatted', 'deeplineagent', { model: "openai/gpt-5.4-mini", prompt: "<exact Clay prompt with {{field}} refs translated>" }) // larger structured output .withColumn('strategic_summary', 'deeplineagent', { model: "anthropic/claude-sonnet-4.6", prompt: "<prompt>", jsonSchema: { type: "object", properties: { response: { type: "string" }, top_5_initiatives: { type: "string" }, top_3_sales_initiatives: { type: "string" } }, required: ["response"], additionalProperties: false } }) ``` Reference structured output fields in downstream passes as `{{col_name.field}}`. If you need deeper nesting, flatten it first. --- ## AI Columns — Claygent (Web Research) ### `use-ai` (useCase: `claygent` or web browsing enabled) **Always two passes.** Never combine broad research + generation in one model step — it is harder to debug and less stable than a split search/synthesis flow. **Pass 1 — Research:** ```ts .withColumn('company_research', 'deeplineagent', { model: "openai/gpt-5.4-mini", prompt: "Use the research context to summarize {{company_domain}} ({{company_name}}). Return JSON with summary, initiatives, and sources.", jsonSchema: { type: "object", properties: { summary: { type: "string" }, initiatives: { type: "string" }, sources: { type: "string" } }, required: ["summary", "initiatives"], additionalProperties: false } }) ``` **Alternative research via exa_search** (deterministic, auditable): ```ts .withColumn('exa_search', 'exa_search', { query: "{{company_name}} {{company_domain}} strategic initiatives GTM 2024 2025", num_results: 5, contents: { text: true, highlights: true } }) ``` **Pass 2 — Generation (a later play column reading `{{company_research}}`):** ```ts .withColumn('strategic_initiatives', 'deeplineagent', { model: "anthropic/claude-sonnet-4.6", prompt: "<Clay prompt translated>\n\nResearch context:\n{{company_research}}", jsonSchema: { type: "object", properties: { top_5_initiatives: { type: "string" }, top_3_sales_initiatives: { type: "string" }, top_3_go_to_market_initiatives: { type: "string" }, new_products: { type: "string" }, hypothesis_of_potential_challenges: { type: "string" } }, required: ["top_5_initiatives"], additionalProperties: false } }) ``` --- ## Octave Actions (proprietary — search for native equivalent first) Octave `ca_*` agents are proprietary Clay integrations. Before defaulting to `deeplineagent`, check whether Deepline has added a native equivalent: ```bash deepline tools search "qualify person ICP" deepline tools search "octave" deepline tools search "sequence runner email" ``` If a native tool exists (e.g. `octave_qualify_person`, `octave_sequence_runner`), use it directly — it will be faster and more accurate than the `deeplineagent` fallbacks below. The patterns below are **fallbacks for when no native tool is available**. ### `octave-qualify-person` ```ts .withColumn('qualify_person', 'deeplineagent', { model: "anthropic/claude-sonnet-4.6", prompt: "Score this prospect against our ICP (0-10 total). ICP: [paste ICP criteria]. Prospect — Title: {{title}}, Company: {{company_name}}, Domain: {{company_domain}}, LinkedIn: {{linkedin_url}}, Initiatives: {{strategic_initiatives}}.\n\nScoring dimensions: persona fit (0-4) + seniority (0-2) + hiring signals (0-2) + strategic fit (0-2).\nTier: A=8-10, B=5-7, C=0-4. Qualified if score>=6.\nReturn JSON.", jsonSchema: { type: "object", properties: { score: { type: "number" }, tier: { type: "string", enum: ["A", "B", "C"] }, qualified: { type: "boolean" }, rationale: { type: "string" }, disqualifiers: { type: "array", items: { type: "string" } } }, required: ["score", "tier", "qualified", "rationale"], additionalProperties: false } }) ``` ### `octave-enrich-person` ```ts .withColumn('person_enriched', 'deeplineagent', { model: "anthropic/claude-sonnet-4.6", prompt: "Research this person using public sources. Name: {{first_name}} {{last_name}}, Title: {{title}}, Company: {{company_name}}, LinkedIn: {{linkedin_url}}. Return JSON with background, career_summary, and notable_achievements.", jsonSchema: { type: "object", properties: { background: { type: "string" }, career_summary: { type: "string" }, notable_achievements: { type: "string" } }, required: ["background", "career_summary"], additionalProperties: false } }) ``` ### `octave-run-sequence-runner` (email generation) Two passes: **Pass 1 — Gather signals (separate enrich call):** ```ts .withColumn('sequence_signals', 'deeplineagent', { model: "openai/gpt-5.4-mini", prompt: "Summarize outbound signals for {{first_name}} ({{title}} at {{company_name}}). Use context: {{qualify_person}} / {{strategic_initiatives}} / {{tension_mapping}}. Return key talking points and pain hypotheses." }) ``` **Pass 2 — Write email:** ```ts .withColumn('email_sequence', 'deeplineagent', { model: "anthropic/claude-sonnet-4.6", prompt: "Write a cold email (subject + body, body <70 words). Recipient: {{first_name}}, {{title}}, {{company_name}}. Signals: {{sequence_signals}}. Tone: casual, direct. No buzzwords.", jsonSchema: { type: "object", properties: { subject: { type: "string" }, body: { type: "string" } }, required: ["subject", "body"], additionalProperties: false } }) ``` --- ## LinkedIn Posts ### `social-posts-get-post-activity-posts-and-shares` Skip for automation unless explicitly needed (run-as-button in Clay). Two options when needed: **HarvestAPI (profile-URL-specific):** Use when you need posts for a specific person's profile URL. Run it per profile inside the owned Play: ```bash deepline tools describe harvestapi_get_profile_posts --schema-only deepline tools execute harvestapi_get_profile_posts --payload '{"profile":"<linkedin_url>","page":1}' --out linkedin-posts.csv ``` There is no supported CrustData post-search replacement. Use `harvestapi_get_profile_posts` only when the target profile URL is known. --- ## Scoring ### `score-your-data` (unconfigured — all input slots blank) Replace with `run_javascript` keyword scorer. **Column names below are placeholders — replace with your actual Clay column aliases from the flatten pass.** ```javascript // $WORKDIR/score_row.js // Replace field names with your actual column aliases (from fields.xxx or top-level) let score = 0; const title = (row['job_title'] || '').toLowerCase(); // ← your title column const signal1 = (row['hiring_signal'] || '').toLowerCase(); // ← your first signal column const signal2 = row['tech_stack']; // ← your second signal column // Adjust keywords and weights to your ICP scoring criteria if (['vp', 'director', 'head of'].some((k) => title.includes(k))) score += 3; if (signal1 && signal1.length > 10) score += 2; if (signal2) score += 2; const tier = score >= 7 ? 'A' : score >= 4 ? 'B' : 'C'; return { score, tier }; ``` --- ## CRM Read and Write Clay ships 27 HubSpot actions plus a Salesforce package, and this file does not enumerate them. CRM read/write mappings, plus the two Clay-native table actions (`lookup-row-in-other-table`, `route-row`), live in [clay-api-surface.md](clay-api-surface.md#crm-read-and-write). Go there for any `actionKey` not mapped above. --- ## Campaign Activation ### `add-lead-to-campaign` (Smartlead) `smartlead_add_leads_to_campaign` does **not** exist. Use `smartlead_api_request` to POST to the leads endpoint: ```bash deepline tools execute smartlead_api_request --payload '{ "method": "POST", "path": "/v1/campaigns/<campaign_id>/leads", "body": { "lead_list": [ { "email": "{{final_email}}", "first_name": "{{first_name}}", "last_name": "{{last_name}}", "company_name": "{{company_name}}", "linkedin_url": "{{linkedin_url}}" } ] } }' ``` Or as a play column: ```ts .withColumn('campaign_push', 'smartlead_api_request', { method: 'POST', path: '/v1/campaigns/<campaign_id>/leads', body: { lead_list: [{ email: '{{final_email}}', first_name: '{{first_name}}', last_name: '{{last_name}}', company_name: '{{company_name}}' }] }, }) ``` ### `add-lead-to-campaign` (Instantly) Correct tool name is `instantly_add_to_campaign` (not `instantly_add_contacts_to_campaign`): ```bash deepline tools execute instantly_add_to_campaign --payload '{ "campaign_id": "<campaign_id>", "leads": [{"email": "{{final_email}}", "first_name": "{{first_name}}", "last_name": "{{last_name}}", "company_name": "{{company_name}}"}] }' ``` ### Other campaign platforms | Platform | Tool | Notes | | ------------------------------ | ----------------------------------------------- | --------------------------- | | HeyReach (LinkedIn sequences) | `heyreach_add_to_campaign` | LinkedIn outreach sequences | | Lemlist | `lemlist_add_to_campaign` | Multi-channel sequences | | Smartlead (verify tool schema) | `deepline tools describe smartlead_api_request` | Use API request endpoint | --- ## Field Reference Translation Clay uses `{{f_0sy80p3xxx}}` field IDs in prompts and formula cells. Steps to translate: 1. Get field list from `GET /v3/tables/{TABLE_ID}` → `fields[].id` + `fields[].name` 2. Build the ID→name map: `f_0sy80p3xxx` → `snake_case(field.name)` 3. In recovered prompts, replace every `{{f_xxx}}` with `{{fields.snake_name}}` (post-flatten) or `{{alias}}` (if it's a prior pass output) 4. Fix Clay formula bugs sometimes present in rendered cell values: - Wrong field reference: `{{last_name}}` where `{{job_title}}` was intended — cross-check against the column name - Unresolved single-brace: `{field_name}` (Clay uses `{{double_braces}}` only) — add the second brace pair 5. Reference rules by output type: | Source column type | Downstream reference | | ------------------------------------ | -------------------------------------- | | `run_javascript` returning a scalar | `{{alias}}` | | `run_javascript` returning an object | `{{alias.field_name}}` | | `deeplineagent` without `jsonSchema` | `{{alias}}` (raw string) | | `deeplineagent` with `jsonSchema` | `{{alias.field_name}}` for flat fields | | Flattened Clay field | `{{fields.snake_name}}` | **Full path:** `{{alias.field}}`, `{{alias.field.nested}}`, and array indices like `{{alias.items[0].field}}` all resolve. A path that does not exist renders empty rather than erroring, so check a sample row when a value comes back blank. --- ## Column Alias Convention Aliases **derive from the actual Clay column name**, not from a fixed list: - Snake_case the Clay column name: "Work Email" → `work_email`, "Strategic Initiatives" → `strategic_initiatives` - Strip leading/trailing spaces and special characters before snake_casing - For multi-step patterns (email waterfall fallbacks), append a short functional suffix: `work_email_li` (LinkedIn fallback), `work_email_valid` (validation gate) **Two reserved structural aliases** (always these names, regardless of Clay column names): | Alias | Purpose | | ------------- | ------------------------------------------------------------- | | `clay_record` | Raw bulk-fetch-records output loaded by the shell fetch pass | | `fields` | Flattened clay_record subfields (run_javascript flatten pass) | **All other aliases come from the Clay schema.** Look up `fields[].name` in `GET /v3/tables/{id}` and snake_case them. Do not invent aliases from any memorized list — if the Clay column is named "Tension Mapping", the alias is `tension_mapping`. If it's named "PVP Messages", it's `pvp_messages`. -
clay-api-surface.md 11.4 KB
# Clay internal API surface Clay has no public API for table config. Everything below is the **internal v3 API** the web app itself calls, recovered from 82MB of recorded HAR traffic across 5 workspaces and re-verified live. Host is `https://api.clay.com`. Auth is the browser session cookie, so `credentials: 'include'` from an `app.clay.com` tab is all you need - never copy the cookie into a script. Treat this as observed behavior, not a contract. Clay can change it without notice. Re-run the miner (below) when something breaks. ## The three you almost always want | Need | Endpoint | | --- | --- | | Every action Clay offers, with input/output schemas | `GET /v3/actions?workspaceId={WS}` | | A workbook's child tables | `GET /v3/workbooks/{WORKBOOK_ID}/tables` | | A workbook's dependency graph | `GET /v3/{WS}/workbooks/{WORKBOOK_ID}/overview` -> `{nodes, edges}` | `/v3/actions` is ~25MB and returns `{actions: [...]}` - 1398 entries in the workspace it was captured from. Each carries `key`, `displayName`, `package`, `categories`, `description`, `inputParameterSchema`, `outputParameterSchema`, `auth`. This is the authoritative answer to "what does Clay action `X` actually take", and it is how you map an unfamiliar `actionKey` instead of guessing. `/overview` returns the workbook's real node/edge graph. Prefer it over hand-deriving a dependency diagram from field configs. ## Tables and records | Method | Path | Query | Notes | | --- | --- | --- | --- | | `GET` | `/v3/tables/{TABLE_ID}` | `includeExtraData`, `extraDataViewId` | Config. Fields are at `.table.fields`, NOT `.fields`. `firstViewId` is at `.table.firstViewId`. | | `GET` | `/v3/tables/{TABLE_ID}/count` | - | `{tableTotalRecordsCount}`. True size. | | `GET` | `/v3/tables/{TABLE_ID}/views/{VIEW_ID}/table-schema-v2` | - | `{tableSchema, exampleRecords}`. exampleRecords carry RENDERED formula/action values - richest prompt source. Capped by Clay (~25-55 rows). | | `GET` | `/v3/tables/{TABLE_ID}/views/{VIEW_ID}/records/ids` | - | `{results: [r_xxx]}`. All ids, no pagination. | | `POST` | `/v3/tables/{TABLE_ID}/bulk-fetch-records` | - | Body `{recordIds: [...], includeExternalContentFieldIds: []}`. Returns `{results}`. Batch ~50. | | `GET` | `/v3/tables/{TABLE_ID}/fieldrun` | - | `{fieldIds}` - which fields have run. | | `GET` | `/v3/workspaces/{WS}/tables/{TABLE_ID}/fields/runstatus` | - | `{statusCountsByField}` - per-field run state. Better than inferring "did this column fire" from cells. | | `GET` | `/v3/tables/{TABLE_ID}/has-overflow-csvs/` | - | Whether the table spilled to CSV. | ## Workbooks | Method | Path | Notes | | --- | --- | --- | | `GET` | `/v3/workbooks/{WORKBOOK_ID}/tables` | Array of child tables (`id`, `name`, `type`, `workbookId`). The entry point for a whole-workbook extract. | | `GET` | `/v3/{WS}/workbooks/{WORKBOOK_ID}` | Workbook metadata. | | `GET` | `/v3/{WS}/workbooks/{WORKBOOK_ID}/overview` | `{nodes, edges}` dependency graph. | Note the inconsistency: `/tables` has no workspace segment, the other two do. That is Clay's shape, not a typo. ## Sources | Method | Path | Query | Notes | | --- | --- | --- | --- | | `GET` | `/v3/sources` | `tableId` | Source config for a table. Recovers filter criteria that a table-config extract does NOT include. | | `GET` | `/v3/sources/{SOURCE_ID}` | - | One source. | | `GET` | `/v3/sources/{SOURCE_ID}/runs` | `limit` | Run history. | If a migration needs to know "what population did this table source", `/v3/sources?tableId=` is the only place that answers it. ## Actions and integrations | Method | Path | Query | Notes | | --- | --- | --- | --- | | `GET` | `/v3/actions` | `workspaceId` | Full catalog, ~25MB. | | `POST` | `/v3/actions/dynamicFields` | - | Resolves fields that depend on a connected account (e.g. HubSpot property lists). | | `GET` | `/v3/app-accounts/types` | - | Every integration type. | | `GET` | `/v3/app-accounts/type/{TYPE}` | - | One integration type. | | `GET` | `/v3/workspaces/{WS}/app-accounts` | - | Connected accounts. | | `GET` | `/v3/workspaces/{WS}/app-accounts/accounts/type/{TYPE}` | `resourceId`, `resourceType` | Connected accounts of one type. | ## Workspace, account, billing | Method | Path | Notes | | --- | --- | --- | | `GET` | `/v3/me` | Current user. | | `GET` | `/v3/my-workspaces` | Workspaces you can reach. | | `GET` | `/v3/workspaces/{WS}` | Workspace metadata. | | `GET` | `/v3/workspaces/{WS}/users` | Members. | | `GET` | `/v3/workspaces/{WS}/permissions` | Your permissions. | | `GET` | `/v3/workspaces/{WS}/subroutines` | Saved subroutines. | | `GET` | `/v3/workspaces/{WS}/trigger-definitions-with-schedule` | Scheduled triggers. Use this to spot tables that need a play with a `cron` binding rather than a plain enrich. | | `GET` | `/v3/workspaces/{WS}/resources/{RESOURCE_ID}` | Resource metadata (`resourceType` query). | | `GET` | `/v3/billingplans/{ID}` | Plan. | | `GET` | `/v3/subscriptions/{ID}` | Subscription. | | `GET` | `/v3/credit-accrual` | Credit accrual. | | `GET` | `/v3/model-pricing/{WS}/base-costs` | Per-model base costs. | | `GET` | `/v3/clayback-analytics` | Usage analytics. | ## CRM read and write `clay-action-mappings.md` covers enrichment actions. CRM actions live here. Clay ships 27 HubSpot actions plus a Salesforce package; these are the keys that show up in real tables. Confirm the Deepline side with `deepline tools describe <tool_id>` before writing a pass. | Clay action | Deepline tool | | --- | --- | | `hubspot-lookup-object`, `hubspot-lookup-contact-v2` | `hubspot_search_objects` | | `hubspot-create-object`, `hubspot-create-company` | `hubspot_create_object` | | `hubspot-update-object` | `hubspot_update_object` | | `hubspot-get-properties` | `hubspot_fetch_properties` | | `hubspot-find-owner` | `hubspot_list_owners` | | `hubspot-retrieve-associations` | `hubspot_batch_read_associations` | | `hubspot-enroll-contact` | Sequencer step, not an enrich column | | `hubspot-crm-objects-source` | Seed the CSV from HubSpot, then enrich | `hubspot-lookup-object` inputs are `objectTypeId`, `fields`, `removeBlankValues`, `limit`. A lookup returning a list interpolates by full path: `{{hs_lookup.results[0].id}}`. ### Clay-native table actions **`lookup-row-in-other-table`** - inputs `tableId`, `fields|targetColumn`, `fields|filterOperator`, `fields|rowValue`. Not a provider call. Export the referenced table to CSV and join in a `run_javascript` pass, or query `customer_db` if the data already lives there. **`route-row`** ("Send table data", Clay Labs) - inputs `type`, `tableId`, `rowData`, `nestedData`, `listData`, `isUpsertDisabled`. Writes rows into a different table, so it is not replicable as an enrich column. Produce a filtered output CSV per destination, or model the fan-out as a play. ## Map by job, not by provider name 287 distinct `actionKey` values show up across real Clay tables, but they collapse into about ten jobs. Clay names an action per provider (19 different phone finders, 15 work-email finders); Deepline expresses the same job as one waterfall that tries providers in order and stops when it finds a value. So do NOT hunt for a one-to-one tool per Clay action. Identify the job, use the waterfall, and let it handle provider order. | Clay actions matching | Job | Deepline | | --- | --- | --- | | anything matching `phone` or `mobile` - `*-find-phone`, `*-find-mobile`, `*-find-phone-number`, `clearout-validate-phone` (21 keys) | mobile phone | `prebuilt/person-to-phone` | | `*-find-work-email`, `find-email-v2`, `icypeas-find-email-v2` (15 keys) | work email | `prebuilt/name-and-domain-to-email-waterfall`, or `prebuilt/person-linkedin-to-email` when you have a LinkedIn URL | | `*-find-personal-email`, `*-personal-email-from-linkedin` (14 keys) | personal email | `prebuilt/personal-email` | | `validate-email`, `*-verify-email`, `*-validate-email` (14 keys) | email validation | `leadmagic_email_validation` or `zerobounce_validate`. Accept `valid`, `valid_catch_all`, `catch_all`; reject `unknown` | | `*-find-linkedin-profile`, `contactout-social-url-from-email` (10 keys) | resolve LinkedIn URL | `prebuilt/person-to-linkedin-harvestapi`, or `prebuilt/personal-email-to-linkedin` from an email | | `enrich-person`, `enrich-person-with-mixrank-v2`, `*-enrich-person` (10 keys) | person enrichment | `leadmagic_profile_search` then `crustdata_v3_person_enrich` | | `enrich-company`, `*-enrich-company`, `crunchbase-enrich-*` (22 keys) | company enrichment | `prospeo_enrich_company` or `crustdata_v3_company_search` | | `use-ai`, claygent variants | AI generation | `deeplineagent` with a `jsonSchema` | | `find-lists-of-*-with-mixrank`, `search-person` | sourcing a new list | `crustdata_v3_company_search`, `dropleads_search_people`, or `prebuilt/company-to-contact` | | `add-lead-to-campaign`, sequencer keys | campaign push | `instantly_add_to_campaign`, `smartlead_api_request` | Keys ending `-validate-auth` are Clay's connection health checks. They are not data columns - ignore them during migration. **Read the column's config, not just its name.** A generic key like `enrich-person` or `*-enrich-person` is often wired as a phone or email finder: check `typeSettings.inputsBinding` for a flag such as `requirePhone`, and check what downstream columns actually consume. Map by what the column produces in that table, not by what its key is called. Clay also chains finders with `conditionalRunFormulaText` so each fires only when the previous missed - that is a waterfall, and it becomes ONE Deepline waterfall play, not one pass per provider. Expect providers with no Deepline equivalent (surfe, zeliq, smarte, lyne, bytemine, clearout all lack one). That is fine and is the reason to map by job: the waterfall covers the outcome with the providers Deepline does have. Those patterns cover about 85% of real-table action usage. The rest is genuine long tail - funding data (`intellizence-`, `harmonic-`, `dealroom-`, `cb-insights-`), web/traffic (`semrush-`, `similarweb-`, `capterra-scrape`), and one-off scrapers. For those, pull the schema from the catalog (below), then `deepline tools search "<what it does>"`. If nothing fits, `deeplineagent` with a `jsonSchema` is the fallback, and `generic_http_request` covers a provider Deepline does not wrap. ## Mapping an unknown Clay action When a table uses an `actionKey` that `clay-action-mappings.md` does not cover, do NOT guess. Pull the catalog and read its schema: ```bash # from an app.clay.com tab, via the bookmarklet console or javascript_tool fetch('https://api.clay.com/v3/actions?workspaceId=' + WS, {credentials:'include'}) .then(r => r.json()) .then(d => d.actions.filter(a => a.key === 'hubspot-lookup-object') .map(a => ({key:a.key, inputs:a.inputParameterSchema}))); ``` Then find the Deepline equivalent with `deepline tools search "<what it does>"` and confirm with `deepline tools describe <tool_id>`. ## Re-mining when Clay changes The endpoint list above came from HAR captures. To regenerate after a Clay update: 1. In Chrome DevTools -> Network, record while you exercise the Clay UI (open a workbook, a table, run a column, open the action picker). 2. Right-click -> "Save all as HAR with content". 3. Run the miner in `scripts/clay-har-miner.py` against the HAR. It normalizes ids into placeholders and emits one row per distinct route with methods, statuses, query params, and payload shapes. Large responses (`/v3/actions` in particular) are often recorded without bodies because of DevTools size limits. Fetch those live from a logged-in tab instead of expecting them in the HAR. -
clay-extraction.md 14.2 KB
--- name: clay-extraction description: 'How to extract Clay table configs via MCP or script. Read only when the user needs to extract from Clay — skip if they already provide an extract JSON.' --- # Clay Table Extraction Use `scripts/clay-extract.py` (bundled at `.skills/deepline-gtm/scripts/clay-extract.py`, also at repo root `scripts/clay-extract.py`) to pull full table configs from Clay's internal API. Extracts: field definitions, action settings (prompts, models, webhook URLs), formula text, conditional run logic, and up to ~36-66 sample records. ## Three extraction paths | Path | When | Steps | | ---------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **Bookmarklet** | A human, or an agent with Claude-in-Chrome | One click on the Clay table tab. Downloads a complete `clay_extract_<table>.json`. Agents run the same source via `javascript_tool` (see below) | | **Claude-in-Chrome MCP** | Running inside Claude Code with the extension | Zero steps. Run the bookmarklet source (below), or `fetch(url, {credentials: 'include'})` ad hoc from the authenticated browser session | | **`clay-extract.py` script** | Standalone, CI, or no MCP | One-time cURL paste for auth, then zero-step extraction | The bookmarklet and the MCP path share one source of truth: `scripts/clay-extract-bookmarklet.js` (in this skill). It hits the current Clay v3 API (`/v3/tables/{id}` for config+fields+views, `table-schema-v2` for the rendered sample rows, `/count`, `records/ids`, and batched `bulk-fetch-records`) and assembles the same shape `clay-extract.py` produces. ## Script setup (one-time) ```bash python3 -m venv .venv/clay-extract .venv/clay-extract/bin/pip install requests # Auth: paste a cURL from any api.clay.com request in Chrome DevTools .venv/clay-extract/bin/python3 scripts/clay-extract.py --auth ``` Session is saved to `.clay-session.json` and reused until it expires (~24h). ## Extraction commands ```bash PYTHON=.venv/clay-extract/bin/python3 # By table URL or ID $PYTHON scripts/clay-extract.py https://app.clay.com/workspaces/502058/workbooks/wb_xxx/tables/t_xxx $PYTHON scripts/clay-extract.py t_0t5pj9mqNnpxxjM6jaV # By workbook URL (resolves to all tables in the workbook) $PYTHON scripts/clay-extract.py https://app.clay.com/workspaces/502058/workbooks/wb_0t5pj9dg5C7fGNTajyw # By name (fuzzy matches workbook and table names) $PYTHON scripts/clay-extract.py --workspace 502058 "Demo Request" ``` Output goes to `tmp/clay_extract_<table_name>.json`. Never overwrites existing files. ## What the extract contains ``` { "_meta": { "extractedAt", "method", "tableId" }, "table": { "id", "name", "workbookId", "workspaceId", "firstViewId", "tableSettings" }, "fields": [ { "id": "f_xxx", "name": "AI Message Generator", "type": "action", // source | formula | action | text | date "typeSettings": { "actionKey": "use-ai", // Clay action type "inputsBinding": [ // Action config (prompts, models, etc.) { "name": "prompt", "formulaText": "You are writing..." }, { "name": "model", "formulaText": "\"claude-sonnet-4-6\"" } ], "formulaText": "...", // Formula/prompt text (for formula fields) "conditionalRunFormulaText": "!!{{f_xxx}}" // Conditional execution } } ], "tableSchema": { ... }, // Schema tree from table-schema-v2 "exampleRecords": [ ... ] // Up to ~36-66 sample rows with cell values } ``` ## Clay Functions (subroutine tables) A **Clay Function** is a reusable subroutine packaged as its own table. Other tables call it with a row of inputs; the function runs its column pipeline and writes the results back to the caller's cell. Recognize this pattern and migrate it as a self-contained play with a typed input contract — not as a batch lead list. **Signature — how to recognize a function table:** | Signal | Field | What it means | | --- | --- | --- | | **Input source** | a `type: "source"` field named "Function inputs", id `f_subroutine_source`, `typeSettings.sourceIds` (and sometimes `taggedSourceType`) | The function's parameter row. Every other column reads `{{f_subroutine_source}}?.["<Input Name>"]`. | | **Work action(s)** | one or more `type: "action"` fields (`use-ai`, `hubspot-lookup-object`, an email finder, etc.) | The actual computation. Full config is in `typeSettings` — see below. | | **Write-back** | a `type: "action"` field with `actionKey: "write-to-cell"` | Returns the function's outputs to the calling cell. Its `inputsBinding` `data` **`formulaMap`** lists every output the function exposes, keyed by output name. **This map is the function's output schema.** | | **Table type** | `table.tableSettings.BLOCK_TYPE == "SUBROUTINE"` + `SUBROUTINE_INPUTS` | When present, this is conclusive. `SUBROUTINE_INPUTS` is the **typed input schema** — an array of `{inputName, optional, semanticTypeEnum, description}` (e.g. `{"inputName":"Domain","optional":false,"semanticTypeEnum":"company-domain"}`). Use it verbatim as the play's input contract. | **Fallback when `tableSettings` is absent.** Some extracts (older script runs, trimmed table objects) drop `tableSettings`, so `BLOCK_TYPE` may be missing even for a real function. In that case, detect the function by the **field signature** alone: a `f_subroutine_source` "Function inputs" source field **plus** a `write-to-cell` action is sufficient. Derive the input schema from the distinct `{{f_subroutine_source}}?.["…"]` keys referenced across columns, and the output schema from the `write-to-cell` `data.formulaMap`. Do not require `BLOCK_TYPE` to proceed. **Where the function's contract lives in the extract:** - **Input schema** → `table.tableSettings.SUBROUTINE_INPUTS` when present; otherwise the distinct `{{f_subroutine_source}}?.["First Name"]` / `?.["Domain"]` keys across the columns. - **Output schema** → the `write-to-cell` action's `inputsBinding` entry `name: "data"` → `formulaMap` (each key is an output name; each value is the `{{f_xxx}}` column that fills it). - **Body** → every non-source, non-`write-to-cell` field, in dependency order. Actions carry `actionKey`, `actionVersion`, `actionPackageId`, `authAccountId`, `conditionalRunFormulaText`, `inputsBinding` (verbatim prompts, models, JSON schemas), and — when present — `optionalPathsInInputs` and `customRateLimitRules`. Extracted `formula` fields carry `extractedField.{fieldIdExtractedFrom,extractedKeyPath}` and `mappedResultPath`; waterfall fields carry `typeSettings.formulaWaterfall` (an ordered fallback array). **Migration mental model:** a function table → **one Deepline play** whose input is the subroutine's declared parameter row (not a scraped lead CSV), whose body is one `.withColumn(...)` per non-source field in dependency order, and whose output projection is exactly the `write-to-cell` `data` map. **Drop the `write-to-cell` action itself** — it is Clay-internal plumbing to return values to the caller; a Deepline play persists to its own Customer DB table and returns its columns directly. Callers of the Clay function become callers of the play. **The bookmarklet is the recommended path for function tables.** It passes the whole table object through, so `tableSettings` (hence `BLOCK_TYPE` and `SUBROUTINE_INPUTS`) is captured. The `clay-extract.py` script keeps `tableSettings` too, but if you inherit a trimmed extract, use the field-signature fallback above. Function **config** never needs the HAR — `GET /v3/tables/{id}` carries the full definition. The HAR / `bulk-fetch-records` only add rendered output cell values for parity validation, and are subject to record truncation (`_meta.truncated`), which never drops a field or action config. When an extract has **no sample cell values**, mark any architecture or parity claim `config-inferred, unvalidated` and defer conclusions that require 3+ real records. ## Key Clay API endpoints (undocumented, reverse-engineered) | Endpoint | Method | Returns | | ------------------------------------------------------- | ------ | ----------------------------------------------------------------- | | `/v3/tables/{TABLE_ID}` | GET | Full table config: fields, typeSettings, prompts, action bindings | | `/v3/tables/{TABLE_ID}/views/{VIEW_ID}/table-schema-v2` | GET | Schema tree + example records (up to ~66 rows) | | `/v3/workbooks/{WB_ID}/tables` | GET | List of tables in a workbook `[{id, name, ...}]` | | `/v3/workspaces/{WS_ID}/resources_v2/` | POST | Top-level workspace resources (folders, workbooks) | | `/v3/tables/{TABLE_ID}/views/{VIEW_ID}/records/ids` | GET | All record IDs (for full data pull) | | `/v3/tables/{TABLE_ID}/bulk-fetch-records` | POST | Full cell data for specific record IDs | All require `Cookie: claysession=...` + `origin: https://app.clay.com` headers. ## Important details - **Formula text location**: `field.typeSettings.formulaText` (NOT `field.formulaText`) - **Action prompts**: `field.typeSettings.inputsBinding` array → find entry with `name: "prompt"` → `.formulaText` - **Model**: same array → `name: "model"` → `.formulaText` (e.g. `"claude-sonnet-4-6"`) - **Field references in formulas**: `{{f_xxx}}` format — map to names via the fields array - **Folder URLs** (`/home/f_xxx`): the `f_xxx` is a folder ID, not a field. Folder children aren't exposed via API — use workbook URLs or name search instead. - **Cookie security**: `.clay-session.json` is gitignored. Never log or embed cookies in scripts. ## MCP extraction (for agents with Claude-in-Chrome) When Claude-in-Chrome MCP is available, skip the script. Run the bookmarklet's logic directly, it pulls everything (config, schema, rendered sample rows, all records) in one shot: 1. `tabs_context_mcp` with `createIfEmpty: true` → get a tab 2. `navigate` → the Clay **table view** URL (`.../tables/t_xxx/views/gv_xxx`) 3. Confirm auth before extracting: `javascript_tool` → ```javascript fetch('https://api.clay.com/v3/tables/' + location.pathname.match(/t_[A-Za-z0-9]+/)[0], { credentials: 'include', }).then((r) => r.status); // expect 200; 401 means not logged in ``` 4. Extract: read `scripts/clay-extract-bookmarklet.js` from this skill and paste its IIFE body into `javascript_tool`. It assigns the full result to `window.__clayExtract` and downloads `clay_extract_<table>.json`. - In a headless/automation context the auto-download may not surface a file. Either read the payload off `window.__clayExtract` and write it yourself, or skip the `<a>.click()` and return `JSON.stringify(window.__clayExtract)`. - The payload can be large (the TAL Scoring table is ~14 MB). Return a summary from `javascript_tool` (field counts, `exampleRecords.length`, `recordIds.length`), then pull the full object in chunks or via the download, not as one giant tool result. 5. The browser already has the session cookie. `credentials: 'include'` sends it automatically; without it, fetch returns 401. The cookie is never read by the script, only the browser uses it. **Richest data lives in `exampleRecords`** (from `table-schema-v2`): flat `{ f_xxx: <rendered value> }` rows with formula/action outputs already resolved. `bulkFetchRecords` covers *all* rows but is sparse (only populated cells appear, and un-run rows show just `f_created_at`/`f_updated_at`). For prompt and schema recovery, prefer `exampleRecords` and the `tableSchema` tree; use `bulkFetchRecords` for full-table cell coverage. ## Input data formats When the user provides data directly (not via extraction), these are the possible formats ranked by richness: **Priority: HAR > ClayMate Lite > clay-extract.py output > bulk-fetch-records > schema JSON > user description.** | Input type | Key fields | | -------------------------- | -------------------------------------------------------------------------------------- | | **HAR file** | `bulk-fetch-records` responses with rendered formula cell values — richest | | **ClayMate Lite export** | `.tableSchema` + `.portableSchema` (full prompts even when `bulkFetchRecords` is null) | | **clay-extract.py output** | `.fields[].typeSettings.inputsBinding` for prompts; `.exampleRecords` for samples | | **Schema JSON** | Field names, IDs, action types. No cell values or prompts | | **User description** | Weakest — must approximate everything | **When `bulkFetchRecords` is null:** Fall back to `portableSchema`: - Prompts: `.portableSchema.columns[].typeSettings.inputsBinding` → `{name: "prompt"}` → `.formulaText` - JSON schemas: `{name: "answerSchemaType"}` → `.formulaMap.jsonSchema` (double-escaped — `JSON.parse` twice) - Conditional run: `.typeSettings.conditionalRunFormulaText` **Extract bulk-fetch-records from HAR:** ```bash python3 - <<'EOF' import json, base64, gzip with open('your-export.har') as f: har = json.load(f) for entry in har['log']['entries']: url = entry['request']['url'] if 'bulk-fetch-records' in url: body = entry['response']['content'].get('text', '') enc = entry['response']['content'].get('encoding', '') data = base64.b64decode(body) if enc == 'base64' else body.encode() try: data = gzip.decompress(data) except Exception: pass print(json.dumps(json.loads(data), indent=2)[:5000]) EOF ``` -
contact-accuracy.md 21.2 KB
--- name: contact-accuracy description: 'Use when enriching, scraping LinkedIn, finding or validating B2B emails, checking whether contacts still work at the target company, discovering role-holders at known accounts, or preparing any contact list that will be sent. Triggers on stale titles, same-name matches, job-changers, catch-all emails, malformed final CSV cells, duplicate contacts, or "is this list accurate enough to send?" Skip for pure copywriting with no contact validation.' --- # Contact Accuracy & Freshness Use this for any B2B contact list that will be activated, whatever role the user targets: finance, engineering, sales, clinical, ops, founders, or another function. The role changes only the ICP keep-list in §5. Every other gate is role-agnostic. A row is sendable only when it is the **right person**, in their **current role**, reachable at a **deliverable email at their current employer**. Most bad enrichment rows look complete. The quiet failures are stale titles, same-name strangers, and emails on companies the person already left. The throughline: **a field is only good when you can name its source and the check it passed. Flag everything else.** ## 1. Current role: take the latest active WORK role, not the top-level title LinkedIn scrapers expose a top-level `jobTitle`/`companyName` and a full `experiences[]` array. Reconstruct the current role from `experiences[]`. The top-level title is often a past, secondary, board, or advisory entry because the scraper follows whatever LinkedIn surfaced first. The current role is the experience entry that is **active** and has the **latest start date**: - "Active" = `jobStillWorking === true`, OR no `jobEndedOn`/`endDate`. - Among active entries, pick the one with the **most recent `jobStartedOn`/start date**. - Use `scripts/select-current-role.py` for this; it encodes the rules below and is eval-tested. Don't re-derive the logic inline each time. ### Exclude board, advisory, charity, and retired roles when a real job also exists A person can hold a board seat, advisory role, charity trusteeship, or "Self-Employed" entry alongside their actual operating job. Those are not the current employer for outbound. If the only active entry is board-only, retired, trustee, or similar, flag and HOLD instead of emailing them at the non-operating organization. Deprioritize: `board member`, `member board of directors`, `of the board`, generic `advisor`/`advisory`, `trustee`, `volunteer`, `mentor`, `charity`/`foundation`/`non-profit`, `self-employed`, `retired`, `emeritus`, `ambassador`, and obvious non-profit companies (`foundation`, `trees for`, `rotary`, `united way`, `.org`). Exception: if the user explicitly targets advisor roles, such as Security Advisor, Clinical Advisor, Financial Advisor, or Technical Advisor, treat that title as work when it is at a real operating company. ### Company-name-in-title artifact Scrapers sometimes put the company name in the experience `title` field. If `norm(title) == norm(company)`, recover the real title from the top-level `jobTitle` when it is a work role, or from the leading role phrase in `headline`. `select-current-role.py` handles this fallback chain. ## 2. Identity gate: confirm it's the right person, not a same-name stranger Relaxed searches and name+domain provider lookups routinely return a **different human with the same name**. Two checks decide whether a scraped or searched identity is safe: 1. **Name match.** The returned `firstName`/`lastName` must match the row's name. Allow nicknames via `scripts/validate-linkedin-names.py`, first/last swaps, and maiden-to-married variants when the LinkedIn numeric ID suffix is identical. A non-match means wrong person: reject it. 2. **Company-in-work-history.** The row's target company (or its email domain) must appear **somewhere** in the person's `experiences[]`, current OR past. If the target company appears nowhere in their history, you matched a stranger. The strongest confirmation is domain-anchored provider search by company domain, which should not return a person at a different company. For name-based or scrape results, verify the company appears before trusting the row. When you cannot confirm both, output HOLD/REVIEW with the reason. A guessed identity is not a found contact. ## 3. Freshness: catch job-changers; their email is on a dead domain People move. On older lists, a job-changer's listed email is often on their old company domain, so outreach is both wrong-context and bounce-prone. After you have the current role (§1): - Compare current employer vs the row's listed company. If they differ -> `still_at_company = NO`, and the listed email is suspect. - For NO rows, either (a) re-find a fresh email at the **current** employer's domain, or (b) flag for removal from the campaign. Surface both the old and new company so the user decides. - This is the highest-value check on an *active* campaign list: those are the rows actively getting wrong messaging. ## 4. Email validation: never trust a single provider's status Provider "verified"/"valid" flags are the provider grading its own homework. Independently validate every email with a dedicated validator. ZeroBounce and LeadMagic are current starting hints; confirm live tools with `deepline tools search "email validation" --json` and `describe`. - **valid:** deliverable, ship. - **catch-all:** the domain accepts everything; ship only when a second independent finder returned the exact same address. - **invalid / do_not_mail:** drop. - **unknown / no-status:** retry once, then hold unless a second validator says valid. Validate after recovery/coalescing, once per final address, not during every waterfall leg. ## 5. ICP filter: keep the real targets, hold the drift Title-anchored lists drift, whatever the target role. A list sourced as "X" accumulates people who left that function (a "Heads of Sales" list collects ex-sellers now in ops or consulting; a "DevOps leads" list collects people who moved to management or left the company) or were never in it (a same-name decoy from §2). After resolving the *current* title (§1), bucket each row against **the user's stated ICP**, not against a fixed taxonomy: - **Keep, on-ICP:** the current title matches the target function. Define the match by the user's role family, not by exact string: a "VP Finance **and** Operations" stays on a finance list because finance is present; a "Senior Staff Engineer" stays on an engineering-leaders list. When in doubt about seniority cutoffs, keep and flag rather than drop. - **Keep, decision-maker even if off-title (SMB):** Founder / Co-Founder / CEO / President / Owner / Principal / Managing Partner. At a small company the owner is often the buyer for *any* function, so they belong on most SMB lists regardless of the nominal target role. Confirm this fits the campaign; some want only titled function-holders. - **Hold, off-ICP or adjacent:** a current title in a *different* function than the target (the ops person on a finance list, the recruiter on an engineering list), plus clear non-targets (a job that has nothing to do with the campaign, board-only, retired, student). Send to REVIEW with the reason; don't drop silently. The point is role-agnostic: resolve the current title, then keep the rows whose current function matches what the user actually asked for. **Worked example (finance list):** keep CFO / Chief Financial Officer / Chief Accounting Officer / Controller / Comptroller / VP·SVP·EVP Finance / Director of Finance / Treasurer / Finance Manager / Accountant; hold a current COO or Director of Operations with no finance in the title. Swap that keep-list for the user's actual target (sales leaders, engineering managers, marketing heads, clinicians, whatever) and the same machinery applies. Always *show* which bucket each row fell in so the user can correct the line. ## 6. Lineage: every field names its source A reviewer (and you, later) must be able to see **where each value came from**. Per the export contract, carry `source`, `status`, and `miss_reason`, and for accuracy-critical lists, go further: tag the source *per field*, not just per row. Useful columns: `title_source`, `email_source`, `identity_confirmation` (how you confirmed the person works there: `crustdata_domain` / `linkedin_experience` / `email_domain_match` / `NONE`), `email_validation` (`zerobounce:valid` / `leadmagic:valid` / `catch-all:2-provider`), and `still_at_company`. When you coalesce multiple sources, record which source won each field. This is what lets the user trust 460 rows without re-checking each one, and lets you debug the wrong ones. ## 7. Golden record: one authoritative value per field, all signals kept When you've pulled the same field from several sources (a LinkedIn scrape, Crustdata, PDL, the original list), the deliverable should not make the user guess which to believe. Produce a **golden record**: for each field, one chosen value plus an explicit **source-of-truth** and **confidence**, with every upstream signal preserved in its own column. The golden columns are the answer; the upstream columns are the evidence. Where the golden disagrees with an upstream value, that's intentional, and visible. Build it deterministically: - **Precedence, not averaging.** Define an explicit order and take the first source that has a value, e.g. for a *current title*: fresh LinkedIn scrape (active-work-role logic) -> PDL (only when the scrape failed) -> original list (unverified, last resort). Record which one won in `<field>_source_of_truth`. - **Confidence tier per row**, derived from the winning source + the gates it passed: `HIGH` (fresh first-party source, all gates passed), `MEDIUM` (fallback source, or catch-all email, or current-role not flagged still-working), `LOW` (only an unverified/original value survived), `HOLD` (failed a gate: board/charity-only, identity mismatch, audit-flagged). The user reads this column to decide what to send vs. review. - **Keep upstream signals as columns, not overwritten.** Name them by source: `src_*` (original list), `li_scrape_*`, `pdl_*`, `disc_*` (domain-anchored discovery), `gap_*` (recovery waterfall), `audit_*`, `history_*`. This is what makes a wrong golden value debuggable and lets a human override with full context. - **Surface disagreement.** A `sources_disagree` flag (golden title/company conflicts with another source that also had data) points the reviewer straight at the rows most worth a look. Don't hide conflicts behind the golden value. A golden record done this way means the user can trust the `GOLD_*` columns for a bulk action and still audit any single row down to the provider call that produced each cell. Pair it with a short **data dictionary** (one line per column: what it is, its source, valid values) so the file is self-describing. ## 8. Company-first discovery: when you have the accounts, anchor on them, not the names §2 said domain-anchored search is the strongest identity confirmation. When your input is a list of **known accounts** (companies + domains) and the goal is "the person in role X at each" (the CFO, the VP Eng, the Head of Sales, the office manager, whatever the campaign targets), invert the whole pipeline: don't start from a name you're trying to verify, start from the company and *discover* the current role-holder. Because you never matched on a name, the **same-name-decoy class from §2 can't occur** here: a domain-anchored search can't hand you a stranger who happens to share a name with someone on your list. It can still hand you the wrong *person at the right company* (a former employee whose profile still lists the domain), so §8's verification is about role-freshness, not name-matching. This matters most when the list is stale or title-anchored. In stale-account audits, name-first refresh often keeps the *original* person even after they left; company-first discovery finds the **current** role-holder who replaced them. The same applies to any role: a VP Eng who left is replaced by the new VP Eng, and company-first finds the successor while name-first keeps emailing the person who walked. The company-first row is the one you actually want to email. The pattern, in durable terms: - **Input contract:** company name + domain (+ optional tier). - **Discovery:** domain-anchored people search filtered to the **target role**, across multiple providers in a waterfall: Crustdata by company-domain and a search/finder provider as fallback. Pass the role as a title filter (the user's target function, not hardcoded to finance). Each provider is blind to the others; take the first that returns a current role-holder at that domain. Find the live tool/play names with `deepline plays search "company contacts" --json` and `deepline tools search "people search domain" --json`; confirm input shape with `describe` before running, because provider field names rot. - **Verify the discovered person, but don't name-gate them.** The §2 name-match check does not apply: you *want* a different name than the one on your stale list, so requiring the names to match would reject the successor you came to find. What you owe instead is (a) §1's current-role logic on the returned person, to confirm the company is their *current* employer and not a role they already left, and (b) that the company genuinely appears as their current/recent employer (the domain anchor gives you this for free). Confirm those two, and the discovered name is the answer. - **Coalesce with any name-first data you also have.** When you run both pipelines (company-first discovery + a name-first refresh of the original contact), record per field which pipeline won, and flag where they disagree: that disagreement is exactly the "the original person left, here's the successor" signal worth surfacing. Company-first is the safer default for account lists; fall back to name-first only when you have no reliable domain or the account isn't the unit of work (e.g. a list of individuals with no shared employer). ## 9. Validate every cell, not just every row, before you ship Row gates (§1–§5) catch wrong people and stale roles. They do not catch malformed cells: an email that will not parse, a LinkedIn URL that 404s, a bad date, or a title field that still contains the company name. Run a deterministic audit over the final file before delivery: - **Email** parses as RFC-valid and the domain has a dot. Validity is not deliverability, but malformed strings should never reach the file. - **LinkedIn URL** is a well-formed `linkedin.com/in/...` (or a clearly-marked abbreviated slug), not a search URL or a bare name. - **Dates** match the formats your golden record promises (`YYYY`, `YYYY-MM`, `Mon YYYY`). - **Title ≠ company.** If `norm(title) == norm(company)`, the company name leaked into the title (§1) and the repair didn't take; fix it or flag it. - **Email-domain vs company.** When the email domain isn't an obvious match to the current company (and isn't a known acronym/parent domain), flag it for eyeball: usually a legitimate alternate corporate domain, occasionally a job-changer §3 missed. - **Confidence consistency.** No row should be `HIGH` confidence with an empty title, or claim a source-of-truth it has no column for. Emit row-addressable flags so each issue is reviewable without re-scanning the file. Most flags mean "eyeball this," not "wrong"; the point is that nothing malformed ships silently. ```bash python3 scripts/contact-accuracy-audit.py final.csv > final_audited.csv ``` The audit adds `email_risk`, `profile_age_days`, `email_verification_age_days`, `flags`, `flag_reason`, and `ACTION`. It checks: - profile and email verification freshness, using 30 days as the default freshness SLA - catch-all risk, with `catch-all` allowed to ship only when at least two independent finders returned the same address - job-changer detection and old-domain email detection - company email-domain aliases via `allowed_email_domains` - malformed final cells: email shape, LinkedIn `/in/` URL, and `title != company` - duplicate-person conflicts using LinkedIn URL, then email, then name + company domain + title Eval it with `python3 scripts/contact-accuracy-audit.py --fixtures scripts/fixtures_contact_accuracy_audit.json`. ## 10. The deliverable: one ACTION column, holds sorted to the top A reviewer staring at 400 validated rows needs to know *what to do with each one* faster than they can read a confidence tier and infer it. The format that worked: a single **`ACTION`** column with a small, exhaustive vocabulary, plus a **`flag_reason`** column that says why in one line. Sort so the rows needing a human come first. | ACTION | Means | Maps from | | --- | --- | --- | | `SEND` | Right person, current role confirmed, email is deliverable | HIGH confidence; or MED whose only soft signal is a 2-provider-corroborated catch-all email (§4); `still_at_company` not NO | | `REMOVE / RE-TARGET` | Not safe to email *for this campaign*: either a job-changer (listed email on a dead domain) or a board/charity-only contact (no operating role to target) | §3 `still_at_company=NO`, or §1 board/charity-only HOLD | | `VERIFY` | Couldn't confirm the current role (e.g. unscrapeable profile); eyeball before sending | LOW confidence / unverified source | | `REVIEW` | Real person, but a soft signal needs a human: current role not flagged still-working, or sources disagree on title | MED confidence, `sources_disagree=YES` | `ACTION` is derived from the golden record's confidence + flags (§7); it's a *decision projection* of columns the file already has, not new judgment. The one place it isn't a pure tier read is the catch-all carve-out: a MED row whose only softness is a corroborated catch-all email is deliverable enough to `SEND` (§4), so don't bucket every MED into `REVIEW`. Keep `flag_reason` specific ("Job-changed: left X, now at Y") so the reviewer trusts the call without re-deriving it. The `REMOVE / RE-TARGET` rows are the highest-value output on an active campaign: the job-changers in that bucket are getting wrong messaging at a company they've left right now. ## 11. Confidence is honest, and some sources don't earn trust Two failure modes that look like success: **Don't promise deliverability you can't guarantee.** Email validity is point-in-time: a mailbox valid at validation can bounce a week later, and a catch-all domain never confirms the specific mailbox at all (§4). A list that's been gated through §1–§10 is *as accurate as the current data allows*, which is the honest claim. Stating it that way, rather than "zero mistakes" or "100% deliverable", sets the right expectation and is what the user actually relies on. Overclaiming erodes trust the first time a verified row bounces. **A provider that returns *a* person isn't a provider that returns the *right* person.** Name+domain lookups (PDL and similar) for contacts with no LinkedIn will confidently return a same-name human who isn't your target: the §2 decoy problem, just from a structured provider instead of a search. These are not safe to ship as confident rows; they need the same name+company-in-history gate, and when the gate can't confirm, they belong in HOLD/`VERIFY`, not in `SEND`. "The provider returned something" is not confirmation; "the returned person's history contains the target company" is. ## Putting it together (recommended order) For a contact list you will send: 1. **Source company-first when you can** (§8). For known accounts, anchor on company + domain and discover the current role-holder. Verify by current role, not by name match, because a successor may be the correct answer. See also `recipes/account-orgchart.md`. 2. **Resolve the current role** with `select-current-role.py` (§1): latest active work role, board/charity excluded, company-name-in-title fixed. 3. **Identity gate the names you *brought in*** (§2): name match + company-in-history. This is for verifying a name you already have (a scrape or a name-first refresh) and applies to structured-provider results too (PDL by name+domain), not just searches (§11). It does *not* apply to company-first discoveries from step 1. 4. **Freshness** (§3): flag job-changers; re-find at the current domain or hold. 5. **Find + validate email** (§4): waterfall to find, ZeroBounce/LeadMagic to confirm, catch-all needs corroboration. 6. **ICP filter** (§5): keep rows whose current title matches the user's target role (+ SMB owners); hold off-ICP/adjacent-function rows. 7. **Emit lineage** (§6): per-field source, validation, identity confirmation, still_at_company. 8. **Build the golden record** (§7): one chosen value per field with source-of-truth + confidence, upstream signals kept as columns, plus a data dictionary so the file is self-describing. 9. **Validate every cell** (§9): run `contact-accuracy-audit.py` over the final file for email/URL/date shape, `title != company`, domain alignment, freshness, catch-all risk, aliases, and duplicate-person conflicts. 10. **Project to an ACTION column** (§10): `SEND` / `REMOVE / RE-TARGET` / `VERIFY` / `REVIEW` with a one-line `flag_reason`, holds sorted to the top. Anything that fails a gate lands in a non-`SEND` bucket with its reason, never silently shipped. 11. **State confidence honestly** (§11): "as accurate as the current data allows," not "zero mistakes." Email validity is point-in-time. Eval the role selector with `python3 scripts/select-current-role.py --fixtures scripts/fixtures_current_role.json`. Eval the final-row audit with `python3 scripts/contact-accuracy-audit.py --fixtures scripts/fixtures_contact_accuracy_audit.json`. -
monitor-contract-reference.md 27.4 KB
<!-- GENERATED FROM ProviderMonitorCapabilityDefinition; content-sha256: b39e820310b1efc90be25f554d15d9ae1e1982ae86f5799522c89ce69ce5a8dd; run bun run docs:monitor-contract --> # Monitor Contract Reference This factual reference is generated from the same monitor capability contracts used by validation and the live `tools get` / `monitors available` surfaces. Keep rationale and workflows in hand-written guides. ## Shared lifecycle - monitors check validates a definition locally and does not deploy, spend credits, or prove a future event will arrive. - monitors deploy is a full desired definition. Use deploy --dry-run to inspect provider and Deepline-credit effects; use monitors update for a patch. - monitors get returns the stored deployed definition plus monitor_spec, whose fields list the deployable payload paths, descriptions, constraints, and provider-specific semantics for that monitor type. ## Shared errors - Validation errors name the payload path and expected contract value; correct the definition and rerun monitors check. - A paused monitor needs a balance or entitlement correction before reactivation. Deepline exposes Deepline pricing only. ## `attio.crm_events` Managed Attio CRM event ingestion via a Deepline-owned webhook subscription. ### Executable examples #### Record changes ```json { "key": "attio-record-events", "tool": "attio.crm_events", "payload": { "event_types": [ "record.created", "record.updated" ] } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `webhook` | `attio.attio_webhooks` | Managed Attio webhook binding metadata. | | `subscriptions` | `attio.attio_webhook_subscriptions` | Attio event subscriptions configured by this monitor. | | `events` | `attio.attio_events` | Attio CRM webhook events delivered to Deepline. | #### Fields | Field | Semantics | | --- | --- | | `event_types` | Attio webhook event types to ingest. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `findymail.signal_monitor` Creates a Deepline-managed Findymail signal monitor data pipe and writes provider-native signal rows into Customer DB output tables. ### Executable examples #### Track hiring keywords ```json { "key": "job-change-signals", "tool": "findymail.signal_monitor", "payload": { "name": "Job change signals", "signal_type": "keyword_mention", "keywords": [ "hiring", "job change" ], "enrichment_level": "email" } } ``` #### Track target company job changes ```json { "key": "target-company-job-changes", "tool": "findymail.signal_monitor", "payload": { "name": "Target company job changes", "signal_type": "job_change", "target_companies": [ "stripe.com", "OpenAI" ], "icp_filters": { "countries": [ "US" ], "employee_count_ranges": [ "51-200", "201-500" ] } } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `signals` | `findymail.findymail_signals` | Detected Findymail signal rows emitted by the upstream signal monitor. | | `monitor` | `findymail.findymail_signal_monitors` | Upstream Findymail signal monitor metadata persisted after deploy. | #### Fields | Field | Semantics | | --- | --- | | `name` | The upstream Findymail signal monitor name. | | `signal_type` | Findymail signal family to monitor. Accepted values match the createAMonitor OpenAPI schema. | | `keywords` | Keywords to track for keyword_mention, or for post_engagement when no post_url is provided. | | `post_url` | LinkedIn post URL to monitor for post_engagement signals. | | `profile_url` | LinkedIn profile URL to monitor as an alternative post_engagement source. | | `engagement_types` | Engagement types to track for post_engagement monitors. | | `enrichment_level` | Optional Findymail contact enrichment level for matched contacts. | | `lead_list_id` | Optional Findymail lead list id for automatically saving matched contacts. | | `ai_relevance_prompt` | Optional custom prompt used by Findymail to score AI relevance. | | `target_companies` | Company names or domains used to narrow new_hire and job_change signals. | | `is_shared` | Optional opt-in flag to share the monitor with the user's current Findymail team. | | `icp_filters` | Optional ICP criteria used to narrow Findymail signal matching. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `heyreach.campaign_events` Managed HeyReach campaign event ingestion via a Deepline-owned webhook subscription. ### Executable examples #### Reply events ```json { "key": "heyreach-replies", "tool": "heyreach.campaign_events", "payload": { "event_type": "MESSAGE_REPLY_RECEIVED", "campaign_ids": [ 23501, 23502 ] } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `webhook` | `heyreach.heyreach_webhooks` | Managed HeyReach webhook binding metadata. | | `campaigns` | `heyreach.heyreach_webhook_campaigns` | HeyReach campaign scopes configured by this monitor. | | `events` | `heyreach.heyreach_events` | HeyReach webhook events delivered to Deepline. | #### Fields | Field | Semantics | | --- | --- | | `event_type` | HeyReach webhook event type to ingest. | | `campaign_ids` | Optional HeyReach campaign ids to scope this monitor. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `instantly.campaign_events` Managed Instantly campaign event ingestion via a Deepline-owned webhook subscription. ### Executable examples #### Interested leads ```json { "key": "instantly-interested-leads", "tool": "instantly.campaign_events", "payload": { "event_type": "lead_interested", "campaign_id": "camp_123" } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `webhook` | `instantly.instantly_webhooks` | Managed Instantly webhook binding metadata. | | `webhook_events` | `instantly.instantly_webhook_events` | Instantly webhook events delivered to Deepline. | #### Fields | Field | Semantics | | --- | --- | | `event_type` | Instantly webhook event type to ingest. | | `campaign_id` | Optional Instantly campaign id to scope the webhook. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `lemlist.campaign_events` Managed Lemlist campaign event ingestion via a Deepline-owned webhook subscription. ### Executable examples #### Campaign replies ```json { "key": "lemlist-replies", "tool": "lemlist.campaign_events", "payload": { "event_type": "emailsReplied", "campaign_id": "cam_123" } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `webhook` | `lemlist.lemlist_webhooks` | Managed Lemlist webhook binding metadata. | | `campaign_events` | `lemlist.lemlist_campaign_events` | Lemlist campaign events delivered to Deepline. | #### Fields | Field | Semantics | | --- | --- | | `event_type` | Lemlist campaign webhook event type to ingest. | | `campaign_id` | Optional Lemlist campaign id to scope the webhook. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `rb2b.visitor_events` Ingest RB2B identified-visitor webhook events after you add the Deepline endpoint in RB2B. ### Executable examples #### Website visitors ```json { "key": "rb2b-website-visitors", "tool": "rb2b.visitor_events", "payload": {} } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `events` | `rb2b.visitor_events` | RB2B identified visitor webhook events delivered to Deepline. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `snitcher.website_sessions` Capture signed, session-based Radar website activity from the Deepline Analytics tracker. ### Executable examples #### Website sessions ```json { "key": "deepline-analytics-sessions", "tool": "snitcher.website_sessions", "payload": { "domains": [ "example.com" ] } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `sessions` | `deepline_analytics.sessions` | Signed, session-based Deepline Analytics events. | #### Fields | Field | Semantics | | --- | --- | | `domains` | Provider-specific domains monitor filter. | | `formTracking` | Provider-specific formTracking monitor filter. | | `clickTracking` | Provider-specific clickTracking monitor filter. | | `customEvents` | Provider-specific customEvents monitor filter. | | `waitForConsent` | Provider-specific waitForConsent monitor filter. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `smartlead.campaign_events` Managed Smartlead campaign event ingestion via a Deepline-owned campaign webhook subscription. ### Executable examples #### Campaign replies ```json { "key": "smartlead-replies", "tool": "smartlead.campaign_events", "payload": { "campaign_id": 372, "event_types": [ "EMAIL_REPLY" ] } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `webhook` | `smartlead.smartlead_webhooks` | Managed Smartlead webhook binding metadata. | | `subscriptions` | `smartlead.smartlead_webhook_event_subscriptions` | Smartlead event subscriptions configured by this monitor. | | `outreach_events` | `smartlead.smartlead_outreach_events` | Smartlead outreach events delivered to Deepline. | #### Fields | Field | Semantics | | --- | --- | | `campaign_id` | Smartlead campaign id to scope this webhook monitor. | | `event_types` | Smartlead campaign webhook event types to ingest. | | `categories` | Optional Smartlead categories for category-based webhook events. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `deepline_native.company_radar` Creates a Deepline Native company radar data pipe and writes Deepline Native company event rows into Customer DB output tables. ### Executable examples #### Track company job openings ```json { "key": "job-openings", "tool": "deepline_native.company_radar", "payload": { "domain": "stripe.com", "radar_type": "company_job_openings" } } ``` #### Filter new-hire tracking by title and department ```json { "key": "exec-hires", "tool": "deepline_native.company_radar", "payload": { "domain": "stripe.com", "radar_type": "company_new_hires", "departments": [ "Engineering" ], "seniorities": [ "Director", "Vice President" ], "job_titles": "\"VP Engineering\" OR \"Head of Product\"" } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `company_job_openings` | `deepline_native.deepline_native_company_job_openings` | Streams new job postings at a company into your warehouse and triggers plays. | | `company_promotions` | `deepline_native.deepline_native_company_promotions` | Streams internal promotions at a company into your warehouse and triggers plays. | | `company_mentions` | `deepline_native.deepline_native_company_mentions` | Streams news and web mentions of a company into your warehouse and triggers plays. | | `company_new_hires` | `deepline_native.deepline_native_company_new_hires` | Streams new hires at a company into your warehouse and triggers plays. | | `company_reviews` | `deepline_native.deepline_native_company_reviews` | Streams new employer and product reviews of a company into your warehouse and triggers plays. | | `company_social_posts` | `deepline_native.deepline_native_company_social_posts` | Streams new social posts from a company into your warehouse and triggers plays. | | `company_social_engagements` | `deepline_native.deepline_native_company_social_engagements` | Streams social engagements on a company into your warehouse and triggers plays. | #### Fields | Field | Semantics | | --- | --- | | `job_titles` | Provider-facing title expression. Deepline validates its grammar and forwards it unchanged; it overrides departments and seniorities when present. Stored readback does not prove upstream matching or billing semantics. | | ↳ applies | Only company_new_hires, company_job_openings, company_promotions, and company_social_posts_cxo. | | ↳ precedence | job_titles overrides departments and seniorities. | | ↳ grammar | Double-quoted title terms joined with uppercase AND, OR, and NOT. Parentheses are not part of the documented grammar. | | ↳ grammar example | `"VP" OR "Head of Sales"` | | `departments` | Persona department filter. | | ↳ applies | Only company_new_hires, company_job_openings, company_promotions, and company_social_posts_cxo; ignored when job_titles is present. | | `seniorities` | Persona seniority filter. | | ↳ applies | Only company_new_hires, company_job_openings, company_promotions, and company_social_posts_cxo; ignored when job_titles is present. | | `updates_since` | Use this only to request historical findings; it is not a query-time date filter. Omit it for a future-only start at provider creation. For new-hire and promotion radars, source dates are evaluated at calendar-month precision even though the exact submitted timestamp is preserved in the radar response, so Deepline rejects current-month values for those radar types. During initial engagement history, the parent post’s publication time sets eligibility; recurring engagement delivery uses when Deepline Native first discovers the engagement. | | ↳ applies | Calendar-month source-date precision applies to company_new_hires and company_promotions. The engagement timing rule applies to initial and recurring social-engagement delivery. | | ↳ grammar | RFC3339 timestamp with Z or a numeric UTC offset; now or earlier and within five calendar years. | #### Pricing, identity, and updates - Pricing: Deepline pricing is selected by radar_type and returned by the live monitor contract. Provider spend is not exposed. - Identity: One Deepline monitor identity is radar_type plus domain per organization. - Update: A filter change replaces the upstream radar under the same Deepline monitor key and retains Customer DB rows. - Backfill: Historical matching can arrive during the first 24 hours. A filter update does not request historical findings that would newly match. #### Troubleshooting - **job_titles is rejected:** Use "VP" OR "Head of Sales"; operators must be uppercase. ## `deepline_native.contact_radar` Creates a Deepline-managed contact radar data pipe and writes provider-native contact event rows into Customer DB output tables. ### Executable examples #### Track contact job changes ```json { "key": "contact-job-changes", "tool": "deepline_native.contact_radar", "payload": { "profile_url": "https://www.linkedin.com/in/example", "domain": "stripe.com", "radar_type": "contact_job_changes" } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `contact_job_changes` | `deepline_native.deepline_native_contact_job_changes` | Streams job changes for a tracked contact into your warehouse and triggers plays. | | `contact_social_posts` | `deepline_native.deepline_native_contact_social_posts` | Streams new social posts from a tracked contact into your warehouse and triggers plays. | | `contact_social_engagements` | `deepline_native.deepline_native_contact_social_engagements` | Streams social engagements by a tracked contact into your warehouse and triggers plays. | #### Fields | Field | Semantics | | --- | --- | | `radar_type` | Deepline contact radar output family. The selected value determines the derived output table. | | `profile_url` | Contact profile URL used to create or seed the upstream radar. | | `domain` | Company domain where the contact works. Required for contact_job_changes (the company the contact is tracked at). | | `email` | Contact email address, used alongside profile_url/full_name to seed contact_job_changes tracking. | | `full_name` | Contact full name, used alongside profile_url/email to seed contact_job_changes tracking. | | `updates_since` | Optional permanent radar starting point. Use an RFC3339 timestamp with a time zone to receive qualifying historical findings from that instant. Omit to start at radar creation. The timestamp must be in the past and within five calendar years. Historical processing can continue for the first 24 hours. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: One monitor identity uses radar_type, profile_url. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `deepline_native.industry_radar` Creates a Deepline-managed industry radar data pipe and writes provider-native industry event rows into Customer DB output tables. ### Executable examples #### Track industry mentions ```json { "key": "ai-industry-mentions", "tool": "deepline_native.industry_radar", "payload": { "industry": "artificial intelligence", "radar_type": "industry_mentions" } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `industry_mentions` | `deepline_native.deepline_native_industry_mentions` | Streams news and web mentions across an industry into your warehouse and triggers plays. | | `industry_job_openings` | `deepline_native.deepline_native_industry_job_openings` | Streams new job postings across an industry into your warehouse and triggers plays. | | `industry_funding_rounds` | `deepline_native.deepline_native_industry_funding_rounds` | DeeplineNativeIndustryFundingRoundsRow | | `industry_funding_references` | `deepline_native.deepline_native_industry_funding_references` | DeeplineNativeIndustryFundingReferencesRow | #### Fields | Field | Semantics | | --- | --- | | `radar_type` | Deepline industry radar output family. The selected value determines the derived output tables. | | `industry` | Industry or market segment used to create or seed the upstream radar. | | `countries` | Optional for 'industry_job_openings' only. Array of country names to filter job postings by location. Maximum 5 countries allowed. Examples: 'United States', 'Canada', 'United Kingdom'. | | `updates_since` | Optional permanent radar starting point. Use an RFC3339 timestamp with a time zone to receive qualifying historical findings from that instant. Omit to start at radar creation. The timestamp must be in the past and within five calendar years. Historical processing can continue for the first 24 hours. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: One monitor identity uses radar_type, industry. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `theirstack.saved_search_webhook` Creates a Deepline-managed TheirStack saved search and webhook data pipe, then writes provider-native saved search, webhook, and event rows into Customer DB output tables. ### Executable examples #### Track new sales engineering jobs ```json { "key": "their-stack-sales-engineering-jobs", "tool": "theirstack.saved_search_webhook", "payload": { "type": "jobs", "name": "Sales engineering jobs", "body": { "job_title_or": [ "Sales Engineer" ], "job_country_code_or": [ "US" ], "posted_at_max_age_days": 7, "include_total_results": false } } } ``` #### Track companies matching hiring filters ```json { "key": "their-stack-hiring-companies", "tool": "theirstack.saved_search_webhook", "payload": { "type": "companies", "name": "Hiring companies", "body": { "company_country_code_or": [ "US" ], "job_title_or": [ "Account Executive" ], "include_total_results": false } } } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `saved_search` | `theirstack.theirstack_saved_searches` | Managed TheirStack saved search metadata persisted after deploy. | | `webhook` | `theirstack.theirstack_webhooks` | Managed TheirStack webhook binding metadata persisted after deploy. | | `job_events` | `theirstack.theirstack_job_events` | TheirStack job webhook events delivered to Deepline. | | `company_events` | `theirstack.theirstack_company_events` | TheirStack company webhook events delivered to Deepline. | #### Fields | Field | Semantics | | --- | --- | | `type` | TheirStack saved search type. Jobs produce job event rows; companies produce company event rows. | | `body` | TheirStack saved search filter body. For type "jobs", use job search filters. For type "companies", use company search filters. | | `name` | Optional upstream saved search name. Deepline generates one from the monitor key when omitted. | | `description` | Optional upstream webhook description. Deepline generates one from the monitor key when omitted. | | `is_alert_active` | Whether TheirStack email alerts are active for the saved search. Defaults to false for Deepline data-pipe monitors. | | `listening_start_time` | Optional ISO timestamp for when the webhook should start listening. Omit to let TheirStack use its default behavior. | | `trigger_once_per_company` | For job saved searches, collapse multiple matching jobs from the same company into one webhook event when true. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. ## `vector.visitor_events` Ingest Vector contact.visited events after you add the Deepline callback URL to a live Vector segment. ### Executable examples #### High-intent website visitors ```json { "key": "vector-high-intent-visitors", "tool": "vector.visitor_events", "payload": {} } ``` ### Outputs | Stream | Customer DB table | Meaning | | --- | --- | --- | | `events` | `vector.visitor_events` | Vector contact.visited events delivered to Deepline. | #### Pricing, identity, and updates - Pricing: Use the Deepline pricing returned by tools get, monitors available, check, or deploy --dry-run. Provider spend is not exposed. - Identity: This monitor uses the provider capability identity declared by Deepline. - Update: Use monitors update for a patch or deploy for a complete desired definition. - Backfill: Existing Customer DB rows are retained; this capability does not promise provider backfill unless its provider documentation says otherwise. #### Troubleshooting - **Validation failed:** Correct the reported payload path, then run monitors check again. -
monitor-sdk.md 2.8 KB
# Monitor SDK example Use the SDK when monitor lifecycle belongs in a script, agent loop, or play repository. The CLI and SDK are two surfaces over the same product model. `defineMonitor` gives the authoring shape type checking; it does not remove the access gate, approval, dry-run, or durable read-back requirements. ```ts import { DeeplineClient, defineMonitor } from 'deepline'; const client = new DeeplineClient(); const access = await client.monitors.status(); if (!access.has_access) throw new Error(access.reason ?? 'No monitor access'); // Read the live job-opening variant before authoring a definition. await client.getTool('deepline_native.company_job_openings'); function storedPayload( detail: Record<string, unknown>, ): Record<string, unknown> { const definition = detail.definition; if (!definition || typeof definition !== 'object') { throw new Error('Monitor read-back did not include a definition.'); } const payload = (definition as { payload?: unknown }).payload; if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw new Error('Monitor read-back did not include a payload.'); } return payload as Record<string, unknown>; } const monitor = defineMonitor({ key: 'company-job-openings', tool: 'deepline_native.company_radar', name: 'Company job openings', payload: { domain: 'stripe.com', radar_type: 'company_job_openings', job_titles: '"Chief Financial Officer"', }, }); // Safe preflight. await client.monitors.check(monitor); await client.monitors.deploy(monitor, { dryRun: true }); // Obtain approval before this write, then prove the stored filter. await client.monitors.deploy(monitor); const stored = await client.monitors.get('company-job-openings'); if (storedPayload(stored).job_titles !== '"Chief Financial Officer"') { throw new Error( 'Monitor deploy did not persist the requested job-title filter.', ); } // The lifecycle verbs mirror the CLI. Read before changing or deleting. await client.monitors.update('company-job-openings', { payload: { job_titles: '"Chief Financial Officer" OR "VP Finance"' }, }); const updated = await client.monitors.get('company-job-openings'); if ( storedPayload(updated).job_titles !== '"Chief Financial Officer" OR "VP Finance"' ) { throw new Error( 'Monitor update did not persist the requested job-title filter.', ); } await client.monitors.delete('company-job-openings', { dryRun: true }); ``` Read-only calls (`getTool`, `status`, `available`, `check`, `list`, `get`, `dependents`, and a `{ dryRun: true }` mutation preview) are safe before approval. `deploy`, `update`, `reactivate`, and `delete` change workspace or provider state and can spend Deepline credits. Use the CLI recipe's approval summary and verify the stored definition after every approved mutation. -
plays-api-reference.md 56.3 KB
# Runtime API Reference Generated from source comments and type declarations by `scripts/generate-play-sdk-reference.ts`. Do not edit this file manually. ## Version And Coverage <!-- prettier-ignore --> | Field | Value | |---|---| | SDK version | `0.3.90` | | SDK HTTP API | `v3` | | Checked-in SDK fallback | `0.3.90` | | Minimum supported SDK | `0.1.53` | | Deprecated below | `0.3.1` | | Generated sources | `apps/deepline-api/src/lib/sdk/api-routes.ts`<br />`packages/sdk/src/types.ts`<br />`packages/sdk/src/client.ts`<br />`packages/sdk/src/release.ts` | | Coverage | HTTP and SDK client surface for runtime calls: health, tool/provider discovery and execution, customer data queries, play runs, play definitions, play artifacts, files, and run inspection. | | Not covered | Provider-specific schemas, dashboard-only UI routes, billing/auth setup guides, and tutorial prose. Provider-specific schemas are returned by the generated tool describe routes. | ## Best Current Pattern Strong runtime API references lead with base URL, auth, version/contract metadata, language examples, and exact generated route tables. Deepline follows that shape here: use the quick call flows first, then the generated route and type tables below for contract details. ## Quick Call Flow 1. `POST /api/v2/plays/run` with a saved/prebuilt `name` and JSON `input`. 2. Read `workflowId` from the response. Treat it as the public run id. 3. Poll `GET /api/v2/runs/:runId` or stream `GET /api/v2/runs/:runId/tail`. 4. Stop when `status` is `completed`, `failed`, or `cancelled`. 5. Read final user output from `result` or the compact `package.outputs` object. Use the CLI or TypeScript SDK for local file compilation and artifact upload. Raw HTTP is best for backend services, Python jobs, schedulers, notebooks, and warehouses that invoke an already-saved or prebuilt play. ## Tool And Provider Call Flow 1. `GET /api/v2/tools/search?q=...` to discover ranked provider/tool candidates. 2. `GET /api/v2/integrations/:toolId/get` to inspect input schema, pricing, extractors, and examples. 3. `POST /api/v2/integrations/:toolId/execute` with `payload` to execute the provider-backed tool. 4. Read normalized data from `toolResponse.raw`, `extractedValues`, and `extractedLists`. Do not expose provider spend; customer-visible billing is Deepline credits/USD only. Inside a play, prefer `ctx.tools.execute(...)` so calls are durable, idempotent, and recorded in run progress. From a regular SDK process, use `Deepline.connect().tools.execute(...)` or `client.executeTool(...)`. ## Authentication Use the Deepline host plus a workspace API key from a trusted backend environment. ```bash export DEEPLINE_HOST_URL="${DEEPLINE_HOST_URL:-https://code.deepline.com}" export DEEPLINE_API_KEY="dl_workspace_key" ``` Every request uses bearer auth: ```http Authorization: Bearer <DEEPLINE_API_KEY> ``` ## Start A Named Or Prebuilt Play ```bash curl -X POST "$DEEPLINE_HOST_URL/api/v2/plays/run" \ -H "Authorization: Bearer $DEEPLINE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "prebuilt/person-linkedin-to-email", "input": { "linkedin_url": "https://www.linkedin.com/in/example-person/" } }' ``` Response: ```json { "workflowId": "play_run_...", "apiVersion": 2, "status": "running", "dashboardUrl": "https://code.deepline.com/dashboard/plays/..." } ``` ## Poll Status ```bash curl "$DEEPLINE_HOST_URL/api/v2/runs/$WORKFLOW_ID?full=true" \ -H "Authorization: Bearer $DEEPLINE_API_KEY" ``` Terminal statuses are `completed`, `failed`, and `cancelled`. `queued`, `running`, and `waiting` are non-terminal. ## Stream Events ```bash curl -N "$DEEPLINE_HOST_URL/api/v2/runs/$WORKFLOW_ID/tail?mode=cli" \ -H "Authorization: Bearer $DEEPLINE_API_KEY" \ -H "Accept: text/event-stream" ``` The stream emits a canonical run snapshot first, then incremental play events until the connection closes or the run reaches terminal state. ## Stop A Run ```bash curl -X POST "$DEEPLINE_HOST_URL/api/v2/runs/$WORKFLOW_ID/stop" \ -H "Authorization: Bearer $DEEPLINE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reason":"caller cancelled"}' ``` ## Python Caller This example is copied from `docs-examples/sdk-v2/http-python/run_prebuilt.py` and compiled by `bun run docs:sdk-v2:check`. Source: `docs-examples/sdk-v2/http-python/run_prebuilt.py` ```python import os import time import json import requests def load_deepline_env(path=".env.deepline"): values = {} if not os.path.exists(path): return values with open(path) as env_file: for line in env_file: stripped = line.strip() if not stripped or stripped.startswith("#") or "=" not in stripped: continue key, value = stripped.split("=", 1) values[key.strip()] = value.strip().strip('"').strip("'") return values deepline_env = load_deepline_env() BASE_URL = os.environ.get( "DEEPLINE_HOST_URL", deepline_env.get("DEEPLINE_HOST_URL", "https://code.deepline.com"), ) API_KEY = os.environ.get("DEEPLINE_API_KEY", deepline_env.get("DEEPLINE_API_KEY")) if not API_KEY: raise RuntimeError("Missing DEEPLINE_API_KEY in .env.deepline") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } start = requests.post( f"{BASE_URL}/api/v2/plays/run", headers=headers, json={ "name": "prebuilt/person-linkedin-to-email", "input": { "linkedin_url": "https://www.linkedin.com/in/example-person/", }, }, timeout=30, ) start.raise_for_status() workflow_id = start.json()["workflowId"] while True: status = requests.get( f"{BASE_URL}/api/v2/runs/{workflow_id}", headers=headers, timeout=30, ) status.raise_for_status() body = status.json() if body.get("status") in {"completed", "failed", "cancelled"}: with open("person-email-result.json", "w") as f: json.dump(body, f, indent=2) print(body) break time.sleep(2) ``` ## Generated Route Tables ### Runtime Health <!-- prettier-ignore --> | Method | Path | SDK/client surface | Purpose | Source | |---|---|---|---|---| | `GET` | `/api/v2/health` | `health` | Check API availability and SDK target health. | `apps/deepline-api/src/app/api/v2/health/route.ts` | ### Tool And Provider Calls <!-- prettier-ignore --> | Method | Path | SDK/client surface | Purpose | Source | |---|---|---|---|---| | `GET` | `/api/v2/executions/by-key/:key` | `executions.getByKey` | Observe or retrieve the retained response for a workspace-scoped tool execution key. | `apps/deepline-api/src/app/api/v2/executions/by-key/[key]/route.ts` | | `GET` | `/api/v2/integrations/:toolId` | `getTool` | Describe one provider-backed tool by integration id. | `apps/deepline-api/src/app/api/v2/integrations/[toolId]/route.ts` | | `POST` | `/api/v2/integrations/:toolId/execute` | `executeTool`<br />`executeToolRaw` | Execute one provider-backed tool call through Deepline. | `apps/deepline-api/src/app/api/v2/integrations/[toolId]/execute/route.ts`<br />`apps/deepline-api/src/app/api/v2/integrations/execute/route.ts` | | `GET` | `/api/v2/integrations/:toolId/get` | `getTool` | Describe one provider-backed tool, including schema, pricing, guidance, and extractors. | `apps/deepline-api/src/app/api/v2/integrations/get/route.ts` | | `POST` | `/api/v2/integrations/:toolId/quote` | `quoteInferenceTool` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/integrations/[toolId]/quote/route.ts`<br />`apps/deepline-api/src/lib/deeplineagent/quote-service.ts`<br />`apps/deepline-api/src/lib/deeplineagent/quote.ts` | | `POST` | `/api/v2/integrations/connect` | `connectNotificationSlack` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/integrations/connect/route.ts` | | `GET` | `/api/v2/integrations/list` | `searchTools` | Compatibility discovery route for integration/tool listing. | `apps/deepline-api/src/app/api/v2/integrations/list/route.ts` | | `GET` | `/api/v2/tools` | `listTools` | List callable provider/tool definitions. | `apps/deepline-api/src/app/api/v2/tools/route.ts` | | `GET` | `/api/v2/tools/providers` | `listProviders` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/tools/providers/route.ts` | | `GET` | `/api/v2/tools/search` | `searchTools` | Search callable provider/tool definitions with ranked metadata search. | `apps/deepline-api/src/app/api/v2/tools/search/route.ts` | ### Customer Data <!-- prettier-ignore --> | Method | Path | SDK/client surface | Purpose | Source | |---|---|---|---|---| | `POST` | `/api/v2/db/query` | `db.query`<br />`queryCustomerDb` | Run a bounded query against the customer data plane. | `apps/deepline-api/src/app/api/v2/db/query/route.ts` | ### Play Runs <!-- prettier-ignore --> | Method | Path | SDK/client surface | Purpose | Source | |---|---|---|---|---| | `GET` | `/api/v2/plays/:name/runs` | `listPlayRuns` | List recent runs for one play. | `apps/deepline-api/src/app/api/v2/plays/[name]/runs/route.ts` | | `GET` | `/api/v2/plays/:name/sheet` | `runs.exportDatasetRows`<br />`getPlaySheetRows` | Read/export runtime sheet rows for a run dataset. | `apps/deepline-api/src/app/api/v2/plays/[name]/sheet/route.ts` | | `POST` | `/api/v2/plays/run` | `startPlayRun`<br />`startPlayRunFromBundle`<br />`runPlay` | Start a saved, prebuilt, or artifact-backed play run. | `apps/deepline-api/src/app/api/v2/plays/run/route.ts` | | `GET` | `/api/v2/runs` | `runs.list`<br />`listRuns` | List runs with filters such as play name and status. | `apps/deepline-api/src/app/api/v2/runs/route.ts` | | `GET` | `/api/v2/runs/:runId` | `runs.get`<br />`getRunStatus`<br />`getPlayStatus` | Read canonical status, result, outputs, and run package, including Runs identified by ctx.runPlayAsync. | `apps/deepline-api/src/app/api/v2/runs/[runId]/route.ts` | | `GET` | `/api/v2/runs/:runId/input` | `runs.input`<br />`getRunInput` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/runs/[runId]/input/route.ts` | | `GET` | `/api/v2/runs/:runId/logs` | `runs.logs`<br />`getRunLogs` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/runs/[runId]/logs/route.ts` | | `POST` | `/api/v2/runs/:runId/observe-grant` | `runs.tail`<br />`tailRun`<br />`runPlay` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/runs/[runId]/observe-grant/route.ts` | | `POST` | `/api/v2/runs/:runId/rerun` | `runs.rerun`<br />`rerun` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/runs/[runId]/rerun/route.ts`<br />`apps/deepline-api/src/lib/plays/rerun-admission-read.ts`<br />`apps/deepline-api/src/lib/plays/rerun-idempotency.ts`<br />`apps/deepline-api/src/lib/plays/scheduler-admission-read.ts` | | `POST` | `/api/v2/runs/:runId/stop` | `runs.stop`<br />`stopRun`<br />`cancelPlay`<br />`stopPlay` | Stop a running or waiting play run. | `apps/deepline-api/src/app/api/v2/runs/[runId]/stop/route.ts` | | `GET` | `/api/v2/runs/:runId/tail` | `runs.tail`<br />`tailRun` | Stream canonical run events over SSE. | `apps/deepline-api/src/app/api/v2/runs/[runId]/tail/route.ts` | | `POST` | `/api/v2/runs/stop-all` | `runs.stopAll`<br />`stopAllRuns` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/runs/stop-all/route.ts` | ### Play Definitions <!-- prettier-ignore --> | Method | Path | SDK/client surface | Purpose | Source | |---|---|---|---|---| | `GET` | `/api/v2/plays` | `listPlays`<br />`searchPlays` | List or search callable plays. | `apps/deepline-api/src/app/api/v2/plays/route.ts` | | `DELETE` | `/api/v2/plays/:name` | `deletePlay` | Delete a saved org-owned play. | `apps/deepline-api/src/app/api/v2/plays/[name]/route.ts` | | `GET` | `/api/v2/plays/:name` | `getPlay`<br />`describePlay` | Describe a saved, shared, or prebuilt play. | `apps/deepline-api/src/app/api/v2/plays/[name]/route.ts` | | `POST` | `/api/v2/plays/:name/history/clear` | `clearPlayHistory` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/history/clear/route.ts` | | `POST` | `/api/v2/plays/:name/live` | `publishPlayVersion` | Promote a revision as the live named play. | `apps/deepline-api/src/app/api/v2/plays/[name]/live/route.ts` | | `GET` | `/api/v2/plays/:name/versions` | `listPlayVersions` | List saved play revisions. | `apps/deepline-api/src/app/api/v2/plays/[name]/versions/route.ts` | ### Play Artifacts <!-- prettier-ignore --> | Method | Path | SDK/client surface | Purpose | Source | |---|---|---|---|---| | `POST` | `/api/v2/plays/artifacts` | `registerPlayArtifact` | Register a bundled play artifact for ad hoc runs. | `apps/deepline-api/src/app/api/v2/plays/artifacts/route.ts` | | `POST` | `/api/v2/plays/check` | `checkPlayArtifact` | Validate a play bundle before storing or running it. | `apps/deepline-api/src/app/api/v2/plays/check/route.ts` | | `POST` | `/api/v2/plays/files/stage` | `stagePlayFiles`<br />`resolveStagedPlayFiles` | Stage CSV or packaged files used by play runs. | `apps/deepline-api/src/app/api/v2/plays/files/stage/route.ts` | ### Management And CLI <!-- prettier-ignore --> | Method | Path | SDK/client surface | Purpose | Source | |---|---|---|---|---| | `POST` | `/api/v2/auth/cli/org-create` | `org create` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/auth/cli/org-create/route.ts` | | `POST` | `/api/v2/auth/cli/organizations` | `org list` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/auth/cli/organizations/route.ts` | | `POST` | `/api/v2/auth/cli/register` | `auth register` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/auth/cli/register/route.ts` | | `POST` | `/api/v2/auth/cli/status` | `auth status` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/auth/cli/status/route.ts` | | `POST` | `/api/v2/auth/cli/switch` | `org set`<br />`org switch` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/auth/cli/switch/route.ts` | | `GET` | `/api/v2/billing/auto-recharge` | `billing.autoRecharge.get`<br />`getTargetAutoRecharge`<br />`billing auto-recharge status` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/auto-recharge/route.ts` | | `PUT` | `/api/v2/billing/auto-recharge` | `billing.autoRecharge.update`<br />`updateTargetAutoRecharge`<br />`billing auto-recharge set|off` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/auto-recharge/route.ts` | | `POST` | `/api/v2/billing/auto-recharge/trigger` | `billing.autoRecharge.trigger`<br />`billing auto-recharge trigger` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/auto-recharge/trigger/route.ts` | | `GET` | `/api/v2/billing/balance` | `billing balance` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/balance/route.ts` | | `GET` | `/api/v2/billing/catalog/current` | `billing.plans`<br />`getBillingPlans`<br />`billing plans` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/catalog/current/route.ts` | | `POST` | `/api/v2/billing/checkout` | `billing checkout` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/checkout/route.ts` | | `POST` | `/api/v2/billing/checkout/verify` | `billing redeem` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/checkout/verify/route.ts` | | `POST` | `/api/v2/billing/credit-purchases` | `purchaseTargetBillingCredits` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/credit-purchases/route.ts` | | `POST` | `/api/v2/billing/credit-purchases/recover` | `billing.recoverCreditPurchase`<br />`billing top-up-recover` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/credit-purchases/recover/route.ts` | | `GET` | `/api/v2/billing/invoices` | `billing.invoices.list`<br />`listBillingInvoices`<br />`billing invoices` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/invoices/route.ts` | | `GET` | `/api/v2/billing/ledger` | `billing history` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/ledger/route.ts` | | `DELETE` | `/api/v2/billing/limit` | `billing limit off` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/limit/route.ts` | | `GET` | `/api/v2/billing/limit` | `billing limit` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/limit/route.ts` | | `POST` | `/api/v2/billing/limit` | `billing limit set` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/limit/route.ts` | | `POST` | `/api/v2/billing/plan-transitions` | `transitionTargetBillingPlan` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/plan-transitions/route.ts` | | `GET` | `/api/v2/billing/plans` | `getTargetBillingPlans` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/plans/route.ts` | | `POST` | `/api/v2/billing/portal-sessions` | `createTargetBillingPortalSession` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/portal-sessions/route.ts` | | `GET` | `/api/v2/billing/status` | `getTargetBillingStatus` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/status/route.ts` | | `POST` | `/api/v2/billing/subscription/cancel` | `billing.subscription.cancel`<br />`cancelBillingSubscription`<br />`billing subscription cancel` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/subscription/cancel/route.ts` | | `POST` | `/api/v2/billing/subscription/checkout` | `billing subscribe` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/subscription/checkout/route.ts` | | `GET` | `/api/v2/billing/subscription/status` | `billing.subscription.status`<br />`getBillingSubscriptionStatus`<br />`billing subscription status` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/subscription/status/route.ts` | | `POST` | `/api/v2/billing/top-up` | `billing.topUp`<br />`topUpBillingBalance`<br />`billing top-up` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/top-up/route.ts` | | `GET` | `/api/v2/billing/usage` | `billing usage` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/billing/usage/route.ts` | | `POST` | `/api/v2/cli/feedback` | `feedback` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/cli/feedback/route.ts` | | `POST` | `/api/v2/cli/send-session` | `sessions send` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/cli/send-session/route.ts` | | `POST` | `/api/v2/cli/send-session/chunk` | `sessions send` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/cli/send-session/chunk/route.ts` | | `POST` | `/api/v2/cli/send-session/finalize` | `sessions send` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/cli/send-session/finalize/route.ts` | | `POST` | `/api/v2/ingestion/repair` | `repairIngestionStorage` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/ingestion/repair/route.ts` | | `GET` | `/api/v2/models/describe` | `describeModel` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/models/describe/route.ts`<br />`apps/deepline-api/src/lib/deeplineagent/model-options.ts`<br />`packages/integrations/deeplineagent/generated/provider-options.ts` | | `GET` | `/api/v2/monitors/access` | `monitors status` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/access/route.ts` | | `POST` | `/api/v2/monitors/audit` | `monitors audit` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/audit/route.ts` | | `POST` | `/api/v2/monitors/batch` | `monitors batch submit` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batch/route.ts` | | `GET` | `/api/v2/monitors/batch/:runId` | `monitors batch get` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batch/[runId]/route.ts` | | `POST` | `/api/v2/monitors/batch/deploy` | `monitors batch deploy` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batch/deploy/route.ts` | | `GET` | `/api/v2/monitors/batches` | `monitors named batch resource paths` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batches/[batchId]/route.ts` | | `DELETE` | `/api/v2/monitors/batches/:batchId` | `monitors named batch stop` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batches/[batchId]/route.ts` | | `GET` | `/api/v2/monitors/batches/:batchId` | `monitors named batch get` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batches/[batchId]/route.ts` | | `PUT` | `/api/v2/monitors/batches/:batchId` | `monitors named batch sync` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batches/[batchId]/route.ts` | | `POST` | `/api/v2/monitors/batches/:batchId/start` | `monitors named batch start` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/batches/[batchId]/start/route.ts` | | `POST` | `/api/v2/monitors/check` | `monitors check` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/check/route.ts` | | `POST` | `/api/v2/monitors/deploy` | `monitors deploy` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deploy/route.ts` | | `GET` | `/api/v2/monitors/deployed` | `monitors list` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deployed/route.ts` | | `DELETE` | `/api/v2/monitors/deployed/:key` | `monitors delete` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deployed/[key]/route.ts` | | `GET` | `/api/v2/monitors/deployed/:key` | `monitors get` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deployed/[key]/route.ts` | | `PATCH` | `/api/v2/monitors/deployed/:key` | `monitors update` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deployed/[key]/route.ts` | | `POST` | `/api/v2/monitors/deployed/:key/reactivate` | `monitors reactivate` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deployed/[key]/reactivate/route.ts` | | `POST` | `/api/v2/monitors/deployed/:key/test` | `monitors test` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deployed/[key]/test/route.ts` | | `POST` | `/api/v2/monitors/deployed/:key/validate` | `monitors validate` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/deployed/[key]/validate/route.ts` | | `GET` | `/api/v2/monitors/fleets` | `monitors fleets get (no id)` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/fleets/route.ts` | | `DELETE` | `/api/v2/monitors/fleets/:fleetId` | `monitors fleets deactivate` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/fleets/[fleetId]/route.ts` | | `GET` | `/api/v2/monitors/fleets/:fleetId` | `monitors fleets get` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/fleets/[fleetId]/route.ts` | | `PUT` | `/api/v2/monitors/fleets/:fleetId` | `monitors fleets sync` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/fleets/[fleetId]/route.ts` | | `POST` | `/api/v2/monitors/fleets/:fleetId/reactivate` | `monitors fleets reactivate` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/fleets/[fleetId]/reactivate/route.ts` | | `POST` | `/api/v2/monitors/fleets/check` | `retained fleet definition check for installed clients (no CLI command)` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/fleets/check/route.ts` | | `GET` | `/api/v2/monitors/health` | `monitors health`<br />`monitors audit --watch` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/health/route.ts` | | `GET` | `/api/v2/monitors/jobs` | `monitors jobs resource paths` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/jobs/[jobId]/route.ts` | | `GET` | `/api/v2/monitors/jobs/:jobId` | `monitors jobs get`<br />`monitors jobs wait` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/jobs/[jobId]/route.ts` | | `POST` | `/api/v2/monitors/jobs/:jobId/cancel` | `monitors jobs cancel` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/jobs/[jobId]/cancel/route.ts` | | `GET` | `/api/v2/monitors/jobs/:jobId/logs` | `monitors jobs logs` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/jobs/[jobId]/logs/route.ts` | | `POST` | `/api/v2/monitors/repair` | `monitors repair` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/repair/route.ts` | | `POST` | `/api/v2/monitors/setup` | `monitors deploy (provider-specific post-deploy readback)` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/setup/[tool]/route.ts` | | `POST` | `/api/v2/monitors/sync` | `monitors sync scalar` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/sync/route.ts` | | `GET` | `/api/v2/monitors/tools` | `monitors available` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/monitors/tools/route.ts` | | `GET` | `/api/v2/notifications` | `getNotifications` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/notifications/route.ts` | | `POST` | `/api/v2/notifications` | `createNotification` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/notifications/route.ts` | | `DELETE` | `/api/v2/notifications/:notificationId` | `deleteNotification` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/notifications/[notificationId]/route.ts` | | `PATCH` | `/api/v2/notifications/:notificationId` | `updateNotification` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/notifications/[notificationId]/route.ts` | | `POST` | `/api/v2/notifications/:notificationId/test` | `testNotification` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/notifications/[notificationId]/test/route.ts` | | `GET` | `/api/v2/notifications/slack/channels` | `listNotificationChannels` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/notifications/slack/channels/route.ts` | | `POST` | `/api/v2/plays/:name/pin` | `setPlayPinned` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/pin/route.ts` | | `POST` | `/api/v2/plays/:name/restore` | `restorePlay` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/restore/route.ts` | | `DELETE` | `/api/v2/plays/:name/share` | `unpublishSharePage` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/share/route.ts` | | `GET` | `/api/v2/plays/:name/share` | `getSharePage` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/share/route.ts` | | `PATCH` | `/api/v2/plays/:name/share` | `updateSharePage` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/share/route.ts` | | `POST` | `/api/v2/plays/:name/share` | `publishSharePage` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/share/route.ts` | | `POST` | `/api/v2/plays/:name/share/regenerate` | `regenerateSharePage` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/[name]/share/regenerate/route.ts` | | `POST` | `/api/v2/plays/files/stage/mint` | `stagePlayFiles`<br />`mintStagedPlayFileUploads` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/plays/files/stage/mint/route.ts` | | `GET` | `/api/v2/sdk/compat` | `compat check` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/sdk/compat/route.ts` | | `GET` | `/api/v2/secrets` | `secrets list`<br />`secrets check`<br />`listSecrets` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/secrets/route.ts` | | `POST` | `/api/v2/secrets` | `secrets set`<br />`secrets set --note` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/secrets/route.ts` | | `DELETE` | `/api/v2/secrets/:id` | `secrets delete` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/secrets/[id]/route.ts` | | `PATCH` | `/api/v2/secrets/:id` | `secrets note`<br />`updateSecretNote` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/secrets/[id]/route.ts` | | `POST` | `/api/v2/secrets/:id/test` | `secrets test` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/secrets/[id]/test/route.ts` | | `DELETE` | `/api/v2/settings/notifications` | `disableNotificationSlack` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/route.ts` | | `GET` | `/api/v2/settings/notifications` | `getNotificationSettings` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/route.ts` | | `PUT` | `/api/v2/settings/notifications` | `setNotificationSlack` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/route.ts` | | `GET` | `/api/v2/settings/notifications/channels` | `listNotificationSlackChannels` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/channels/route.ts` | | `GET` | `/api/v2/settings/notifications/dlq` | `listNotificationDlq` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/dlq/route.ts` | | `GET` | `/api/v2/settings/notifications/dlq/:deliveryId` | `getNotificationDlqDelivery` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/dlq/[deliveryId]/route.ts` | | `POST` | `/api/v2/settings/notifications/dlq/:deliveryId` | `updateNotificationDlqDelivery` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/dlq/[deliveryId]/route.ts` | | `PATCH` | `/api/v2/settings/notifications/subscriptions` | `setNotificationSubscriptions` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/subscriptions/route.ts` | | `POST` | `/api/v2/settings/notifications/test` | `testNotificationSlack` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/settings/notifications/test/route.ts` | | `GET` | `/api/v2/usage/events` | `billing.usageEvent`<br />`getBillingUsageEvent` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/usage/events/route.ts` | | `POST` | `/api/v2/workspaces` | `workspaces.create`<br />`org create` | SDK-facing route. | `apps/deepline-api/src/app/api/v2/workspaces/route.ts`<br />`apps/deepline-api/src/lib/workspaces/create-additional-workspace.ts` | ## Recent Compatible API Changes These entries come from the compatible SDK/API change ledger and explain additive changes that did not require an SDK API-contract bump. Each change lives in `apps/deepline-api/src/lib/sdk/compatible-changes/` so concurrent PRs do not edit a shared ledger file. <!-- prettier-ignore --> | Change | Reason | |---|---| | `2026-08-monitor-fleets-beta` | Adds the Monitor Fleets beta as an additive SDK/API/CLI namespace: canonical tagged-JSON fleet authoring helpers, client.monitors.fleets methods, the `deepline monitors fleets` CLI surface, and authenticated /api/v2/monitors/fleets route... | | `2026-08-play-catalog-metadata` | Adds POST /api/v2/plays/:name/pin plus the setPlayPinned SDK method and plays pin\|unpin CLI commands, and exposes derived canonical tool categories on Play catalog reads with an optional categories filter. These are additive catalog capa... | | `2026-08-play-run-input-replay` | Adds authenticated GET /api/v2/runs/:runId/input and POST /api/v2/runs/:runId/rerun routes, plus runs.input/getRunInput/runs.rerun/rerun SDK methods and deepline runs get --input / deepline runs rerun commands. These are additive capabil... | | `2026-07-sdk-enrich-compiler-source-imports` | Resolves shared enrich-plan compiler imports through TypeScript source paths so server-side MCP callers can reuse the same compiler without relying on built JavaScript artifacts. This is an internal build-resolution change: installed CLI... | | `2026-07-play-detached-runtime-progress` | Corrects the customer-visible status and CLI progress wording for a Play that is actively executing in a detached runtime receipt: it reports running rather than waiting, and identifies that execution state instead of incorrectly suggest... | | `2026-07-agent-led-cli-onboarding` | Adds setup, skills, and doctor CLI commands, folder-scoped browser-auth persistence, npm-based installation guidance, and scoped update and verification behavior while retiring the separate mutable SDK shell-installer route. This is comp... | | `2026-07-play-cost-estimates` | Adds an opt-in include_cost_estimates query parameter and optional costEstimate response field to GET /api/v2/plays, and adds the same optional field to GET /api/v2/plays/:name/live. This is additive and backward compatible: route paths,... | | `2026-07-sdk-enrich-direct-tool-runtime-context` | Makes newly published deepline enrich generated plays type their legacy direct-tool helper against the existing DeeplinePlayRuntimeContext tools capability instead of an incompatible hand-written execute signature. This is a compatible l... | ## Public Types ### `ToolDefinition` Summary definition of a callable provider-backed tool. Returned by `DeeplineClient.listTools` and ranked tool search. Use `getTool(toolId)` or the matching HTTP describe route for provider-specific schema, examples, pricing, extraction guidance, and execution metadata. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `toolId` | `string` | Yes | Unique tool identifier used in API calls (e.g. `"dropleads_search_people"`). | | `provider` | `string` | Yes | Provider that backs this tool (e.g. `"hunter"`, `"dropleads"`, `"test"`). | | `displayName` | `string` | Yes | Human-readable name for display. | | `description` | `string` | Yes | What this tool does — suitable for LLM tool descriptions. | | `categories` | `DeeplineToolCategory[]` | Yes | Categorization tags (e.g. `["people", "enrichment"]`). | | `tags` | `string[]` | No | Searchable provider and account-signal tags. | | `operation` | `string` | No | Operation slug within the provider. | | `operationId` | `string` | No | Normalized operation identifier. | | `operationAliases` | `string[]` | No | Alternative names that resolve to this tool. | | `playReference` | ``prebuilt/${string}`` | No | Explicit globally runnable play reference for play-backed catalog entries. | | `hasInputSchema` | `boolean` | No | Whether detailed input schema is available from `tools describe`. | | `hasOutputSchema` | `boolean` | No | Whether detailed output schema is available from `tools describe`. | | `inputSchema` | `Record<string, unknown>` | No | JSON Schema describing the tool's input parameters. | | `outputSchema` | `Record<string, unknown>` | No | JSON Schema describing the tool's output shape. | | `pricing` | `ToolPricingSummary \| null` | No | User-facing pricing summary. Internal provider/settlement costs are intentionally omitted. | | `usageGuidance` | `{ execute?: string; prefer?: string[]; access?: { extractedLists?: { expression?: string; meaning?: string; }; extractedValues?: { expression?: string; meaning?: string; }; rawToolResponse?: { expression?: string; meaning?: string; }; canonicalToolResponse?: { expression?: string; meaning?: string; }; invalidGetterHint?: string; }; toolExecutionResult?: { type?: 'ToolExecutionResult'; toolResponse?: { raw?: string; rawV2?: string; view?: string; responseMeta?: string; meta?: string; }; meta?: string; extractedLists?: \| Array<{ name: string; expression: string; details?: { strategy?: string; rawToolOutputPaths?: string[]; candidatePaths?: string[]; }; }> \| Record< string, { expression: string; details?: { strategy?: string; rawToolOutputPaths?: string[]; candidatePaths?: string[]; }; } >; extractedValues?: \| Array<{ name: string; expression: string; details?: { strategy?: string; rawToolOutputPaths?: string[]; candidatePaths?: string[]; }; }> \| Record< string, { expression: string; details?: { strategy?: string; rawToolOutputPaths?: string[]; candidatePaths?: string[]; }; } >; [key: string]: unknown; }; }` | No | Copyable play-runtime guidance for V2 tool execution results. | | `search_score` | `number` | No | Search relevance score returned by ranked tool search. | | `search_matches` | `Array<{ field: string; value: string; term?: string; }>` | No | Search match snippets returned by ranked tool search. | | `connected` | `boolean` | No | Whether this tool is callable in the current workspace. `false` when a<br />required customer credential is missing or a managed provider is<br />temporarily unavailable. | | `callable` | `boolean` | No | Whether the tool can be executed. Exact lookup may return non-callable deprecated aliases. | | `deprecated` | `boolean` | No | True when callers should migrate this exact tool id to its replacement. | | `deprecation` | `{ replacementToolId: string; message: string; execution?: 'terminal' \| 'forward'; }` | No | Deprecation reason, replacement, and compatibility execution behavior. | | `credentialStatus` | `\| 'managed' \| 'connected' \| 'requires_connection' \| 'deprecated'` | No | Connection status for discovery: `managed` (Deepline-run credentials),<br />`connected` (your own credential is connected), or `requires_connection`<br />(BYO provider not yet connected in this workspace). `deprecated` means<br />connecting credentials will not make the tool callable. | | `requiresOwnCredential` | `boolean` | No | True when the tool requires a customer-provided credential to run. | | `connectionMessage` | `string` | No | Actionable message shown when a connection is required. | ### `ToolSearchOptions` Query options for ranked tool/provider discovery. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `query` | `string` | No | Free-text search query. | | `categories` | `string` | No | Comma-separated category filter such as `company_search` or `email_finder`. | | `searchTerms` | `string` | No | Optional explicit search terms used by agent/CLI callers. | | `searchMode` | `'v1' \| 'v2'` | No | Search algorithm/version. Defaults to the current ranked mode. | | `includeSearchDebug` | `boolean` | No | Include backend debug metadata in the search response. | ### `ToolSearchResult` Ranked tool/provider discovery response. Includes matching tools plus render/action hints used by the CLI and agents. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `tools` | `ToolDefinition[]` | Yes | Ranked matching tools. | | `count` | `number` | No | Count included in this response when available. | | `total` | `number` | No | Total available count when the backend reports it. | | `truncated` | `boolean` | No | Whether results were truncated by server-side limits. | | `query` | `string` | No | Echoed query. | | `categories` | `string[]` | No | Parsed category filters. | | `search_terms` | `string[]` | No | Parsed search terms. | | `search_mode` | `'v1' \| 'v2'` | No | Search mode used. | | `search_fallback_to_category` | `boolean` | No | Whether search fell back to category matching. | | `emptyResult` | `{ reason: string; message: string; suggestions: Array<{ label: string; command: string; }>; }` | No | Explanation and next commands when filters/search succeed but match zero tools. | | `omitted_plays_hint` | `string` | No | Hint explaining omitted play results when searching tools only. | | `commandTemplates` | `{ describe?: string; execute?: string; }` | No | Copyable CLI command templates for follow-up discovery/execution. | | `render` | `{ sections?: Array<{ title: string; lines: string[]; }>; actions?: Array<{ label: string; command: string; }>; }` | No | Pre-rendered sections and actions for CLI/agent display. | ### `ToolExecution` Standard provider/tool execution envelope returned by low-level SDK calls. `toolResponse.rawV2` contains the complete scrubbed provider response; `toolResponse.raw` is derived locally as the legacy provider-result projection. `extractedValues` and `extractedLists` contain Deepline-normalized getters when the tool exposes them. Billing fields are Deepline-facing and must not expose provider spend. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `status` | `string` | Yes | | | `job_id` | `string` | No | | | `meta` | `Record<string, unknown>` | No | | | `toolResponse` | `{ raw: TData; rawV2?: unknown; view?: 'data' \| 'rawV2'; meta?: TMeta; responseMeta?: TMeta; }` | Yes | | | `extractedLists` | `Record<string, unknown>` | No | | | `extractedValues` | `Record<string, unknown>` | No | | | `billing` | `ToolResultBilling` | No | | ### `ExecutionRecovery` Durable state returned for a keyed tool execution. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `idempotencyKey` | `string` | Yes | Stable caller key used to resume or replay this execution. | | `state` | `'running' \| 'completed' \| 'outcome_unknown'` | Yes | Whether the execution is still running, completed, or has an unknown provider outcome. | | `replayed` | `boolean` | Yes | Whether this response came from an existing durable execution. | | `expiresAt` | `string` | No | ISO timestamp after which the completed execution can no longer be replayed. | ### `ExecutionByKeyResult` Durable lookup result for a keyed tool execution. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `executionRecovery` | `ExecutionRecovery` | Yes | Recovery state and the key that owns the execution. | | `toolId` | `string` | Yes | Provider tool associated with the execution. | | `requestId` | `string` | No | Original server-owned billing request ID, allocated before provider dispatch. | | `responseStatus` | `number` | No | HTTP status saved with the original response, when it was terminal. | | `response` | `unknown` | No | Saved execution response, when one is available for replay. | ### `StartPlayRunRequest` Request body for starting a play run via `DeeplineClient.startPlayRun`. Internal/advanced request shape for low-level submission primitives. Most callers should prefer `deepline plays run`, `DeeplineClient.runPlay`, or `Deepline.connect`. Either `name` (for live plays) or `artifactStorageKey` (for packaged ad hoc runs) is required. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `string` | No | Play name for registered revisions. | | `revisionId` | `string` | No | Explicit revision ID when the caller wants a specific saved version. | | `artifactStorageKey` | `string` | No | R2 artifact key for ad hoc artifact-backed runs. | | `sourceCode` | `string` | No | Source snapshot already validated while registering this artifact. | | `sourceFiles` | `Record<string, string>` | No | Source graph snapshots for local helper files included in cloud preflight. | | `description` | `string` | No | Human-readable one-line description for the revision created by file-backed runs. | | `staticPipeline` | `unknown` | No | Static pipeline already produced while registering this artifact. | | `artifactHash` | `string` | No | Artifact content hash already validated while registering this artifact. | | `graphHash` | `string` | No | Static graph hash already validated while registering this artifact. | | `runtimeArtifact` | `Record<string, unknown>` | No | Optional preloaded artifact snapshot for immediate ad hoc execution. | | `compilerManifest` | `PlayCompilerManifest` | No | Compiler manifest for ad hoc graph runs, including imported play dependencies. | | `inputFileUpload` | `unknown` | No | Primary input file bytes for one-shot server-side staging. | | `packagedFileUploads` | `unknown[]` | No | Packaged file bytes for one-shot server-side staging. | | `input` | `Record<string, unknown>` | No | Runtime input passed to the play function as its second argument. | | `inputFile` | `unknown` | No | Staged file reference for the primary input file (e.g. CSV). | | `packagedFiles` | `unknown[]` | No | Additional staged file references (dependencies, data files). | | `force` | `boolean` | No | Compatibility flag; active sibling runs are allowed. | | `forceToolRefresh` | `boolean` | No | Explicit cache-bypass flag for durable dataset and tool-call reuse. | | `maxConcurrentExternalCalls` | `number` | No | Per-run ceiling for concurrently resident provider-tool executions and<br />direct ctx.fetch calls. The server validates the supported range. | | `maxConcurrentRows` | `number` | No | Run-wide default and ceiling for live dataset-map row resolvers. | | `waitForCompletionMs` | `number` | No | Optionally let the start request wait briefly and return a terminal result. | | `profile` | `string` | No | Per-run execution profile override. The server defaults to absurd. The<br />Only `absurd` is accepted; most callers should leave this unset. | | `integrationMode` | `'live' \| 'eval_stub' \| 'fixture'` | No | Optional per-run provider execution mode for eval/smoke runs. | | `fixtureBehavior` | `FixtureBehavior` | No | Fixture-only provider response timing and outcome simulation. | | `runtime` | `PlayRuntimeSelection` | No | Internal runtime estate selection. The app host remains unchanged. | | `testPolicyOverrides` | `Record<string, unknown>` | No | Internal/dev-only runtime policy overrides for black-box durability tests. | ### `PlayRunStart` Response from starting a play run. Internal/advanced payload returned by low-level play submission primitives. Most callers should prefer `deepline plays run`, `DeeplineClient.runPlay`, or `PlayJob.get`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `workflowId` | `string` | Yes | Public Deepline play-run id for tracking this execution. | | `apiVersion` | `number` | No | Public Deepline play-run API version. | | `name` | `string` | No | Play name (echoed back from the request). | | `status` | `string` | No | Initial status (typically `'RUNNING'`). | | `runtimeBackend` | `string` | No | Resolved runtime backend used for this run. | | `contract` | `Record<string, unknown> \| null` | No | Canonical run contract compatibility metadata. | | `dashboardUrl` | `string` | No | Dashboard URL for the named play. | | `finalStatus` | `unknown` | No | Terminal status returned when the start request used a short completion wait. | | `package` | `PlayRunPackage` | No | Canonical compact run package returned by current SDK/API responses. | ### `PlayStatus` Current status of a play execution, returned by `DeeplineClient.getPlayStatus`. Poll this until `status` reaches a terminal state: `'completed'` | `'failed'` | `'cancelled'`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `runId` | `string` | Yes | Public play-run identifier. | | `apiVersion` | `number` | No | Public Deepline play-run API version. | | `name` | `string` | No | Saved play name for this run, when available. | | `revisionId` | `string` | No | Exact saved revision launched for this run, when applicable. | | `playName` | `string` | No | Alias for `name` used by run/result APIs. | | `dashboardUrl` | `string` | No | Dashboard URL for inspecting the play and its run output in the app. | | `status` | `\| 'queued' \| 'running' \| 'waiting' \| 'completed' \| 'failed' \| 'cancelled'` | Yes | Product-level play-run state. | | `outcome` | `PlayRunOutcome` | No | How this run was admitted or recovered, when the server can prove it. | | `recovery` | `{ mode: 'replayed' \| 'forced' \| 'recovered' \| 'joined'; sourceRunId?: string; }` | No | | | `progress` | `PlayProgressStatus` | No | Execution progress with logs and error details. | | `result` | `unknown` | No | Partial or final result. Available once the play returns. | | `rowOutcomes` | `{ completedRows: number; failedRows: number; totalRows: number; hasRowFailures: boolean; }` | No | Terminal row outcome truth. A completed run may still contain failed rows<br />when row-level failure isolation persisted those rows for retry. | | `package` | `PlayRunPackage` | No | Compact typed run package returned by current run status endpoints. | | `outputs` | `PlayRunPackage['outputs']` | No | Compact typed output summaries, mirrored from the run package when present. | | `run` | `{ id?: string; startTime?: string \| null; closeTime?: string \| null; [key: string]: unknown; } \| null` | No | Scheduler-backed run metadata when returned by the status endpoint. | | `resultView` | `unknown` | No | Server-rendered result view metadata for CLI/UI summaries. | | `contract` | `Record<string, unknown> \| null` | No | Canonical run contract snapshot metadata, when available. | | `wait` | `{ kind: 'integration_event' \| 'sleep'; boundaryId?: string; eventKey?: string; until?: number; } \| null` | No | If the run is blocked on a durable boundary, expose the public wait state. | | `next` | `PlayRunPackage['next'] \| Record<string, unknown>` | No | Structured follow-up actions for inspect/query/export. | | `failedLogs` | `{ runId: string; totalCount: number; returnedCount: number; firstSequence: number \| null; lastSequence: number \| null; truncated: boolean; hasMore: boolean; entries: string[]; view?: 'failed'; association?: 'terminal_failure_window' \| 'retained_before_truncation'; warning?: string; next?: { logs: string }; logsTruncated?: boolean; }` | No | Bounded terminal-failure log window requested by `runs.get`. | | `rerunCommand` | `string` | No | Exact ordinary `plays run` command that can rerun a failed execution. | | `billing` | `RunBillingSummary` | No | Projected settled-charge billing for the run. Returned by `runs.get`.<br />`totalCredits`/`providerEvents` describe THIS run only; `rollup` (present<br />with `--full`) carries the true subtree cost including independently<br />executing ctx.runPlayAsync descendants. Deepline credits only — provider<br />spend is never exposed. | | `billingTotalCreditsRollup` | `number` | No | True subtree cost in Deepline credits (this run + every descendant run),<br />mirrored to the top level for convenience. Present only with `--full`. | | `billingChildCredits` | `number` | No | Deepline credits attributable to descendant runs only. Present with `--full`. | | `billingRollupIncomplete` | `boolean` | No | True when the child-run billing rollup could not be fully resolved. | | `childRuns` | `ChildRunSummary[]` | No | Durable summaries of ctx.runPlayAsync children, returned by `runs.get --full`. | | `childRunProjection` | `AsyncChildRunProjection` | No | Child Runs grouped by the authored ctx.runPlayAsync launch key. `complete`<br />is false when the bounded response is a lower bound rather than the full<br />direct-child set; use each opaque `runId` to retrieve a child Run. | ### `PlayRunPackage` Compact canonical package for an inspected play run. This object is designed for SDK/CLI/API consumers that need stable run metadata, output handles, and follow-up actions without reading dashboard internals. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `schemaVersion` | `1` | Yes | Package schema version. | | `kind` | `'play_run'` | Yes | Package discriminator. | | `run` | `{ id: string; playName: string; status: string; dashboardUrl?: string; acceptedAt?: number \| null; updatedAt?: number \| null; startedAt?: number \| null; finishedAt?: number \| null; durationMs?: number \| null; outcome?: PlayRunOutcome; recovery?: { mode: 'replayed' \| 'forced' \| 'recovered' \| 'joined'; sourceRunId?: string; }; error?: string; activity?: PlayRunActivityProjection \| null; }` | Yes | Run identity, status, timing, and dashboard metadata. | | `warnings` | `string[]` | No | Bounded customer-safe warnings about output projection or availability. | | `steps` | `Array<Record<string, unknown>>` | Yes | Step-level summaries emitted by the runtime. | | `outputs` | `Record<string, Record<string, unknown>>` | Yes | Named output summaries, including dataset handles and scalar outputs. | | `datasets` | `Array<{ kind: 'dataset'; datasetId?: string; path: string; tableNamespace?: string; rowCount?: number; sqlTableName?: string; sqlQualifiedTableName?: string; recovered?: true; exportUnavailable?: { reason: 'empty_dataset' \| 'shared_table_namespace'; message: string; }; preview?: Record<string, unknown>; actions?: PlayRunDatasetActions; }>` | No | Every durable Dataset Handle explicitly registered by this run. | | `logs` | `{ tail: string[]; totalCount: number; returnedCount: number; truncated?: boolean; }` | No | Small retained tail of customer and runtime logs; fetch the full stream through `runs.logs`. | | `next` | `{ inspect?: PlayRunActionPackage; full?: PlayRunActionPackage; billing?: PlayRunActionPackage; export?: PlayRunActionPackage; query?: PlayRunActionPackage; logs?: PlayRunActionPackage; }` | No | Follow-up actions a caller can perform against the run. | ### `PlayRunListItem` Summary of a single play run, returned by `DeeplineClient.listPlayRuns`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `workflowId` | `string` | Yes | Public Deepline play-run id. | | `playName` | `string \| null` | No | Saved play name for this run, when available. | | `runId` | `string` | Yes | Backend run attempt id, when exposed. | | `parentRunId` | `string \| null` | No | Parent play-run id when this run was launched through ctx.runPlay. | | `rootRunId` | `string \| null` | No | Root play-run id for nested ctx.runPlay descendants. | | `type` | `string` | Yes | Workflow type (typically `'Workflow'`). | | `status` | `string` | Yes | Human-readable status (e.g. `'Completed'`, `'Failed'`). | | `startTime` | `string \| null` | No | ISO 8601 timestamp when the run started. | | `startedAt` | `number \| string \| null` | No | Unix epoch milliseconds when the run started, returned by normalized V2 run summaries. | | `createdAt` | `number \| string \| null` | No | Unix epoch milliseconds when the run was created. | | `closeTime` | `string \| null` | No | ISO 8601 timestamp when the run finished. | | `finishedAt` | `number \| string \| null` | No | Unix epoch milliseconds when the run finished, returned by normalized V2 run summaries. | | `executionTime` | `string \| null` | Yes | Duration string (e.g. `'2.5s'`). | | `billingTotalCredits` | `number` | No | Total Deepline credits charged for the run, when available. | | `billingMaxCreditsPerRun` | `number \| null` | No | Configured per-run Deepline credit cap, when available. | | `memo` | `{ orgId: string; playName: string; userId: string \| null; }` | Yes | Metadata attached to the workflow. | ### `StopPlayRunResult` Result returned by `DeeplineClient.stopPlay`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `runId` | `string` | Yes | Public play-run identifier the stop request targeted. | | `stopped` | `boolean` | Yes | Whether the server confirmed the run was stopped. | | `cancelling` | `boolean` | No | True when the durable cancellation lane accepted the request and is draining. | | `hitlCancelledCount` | `number` | Yes | Number of open HITL interactions marked cancelled. | | `staleSchedulerState` | `boolean` | No | True when the scheduler state for the run was stale and the stop could<br />not be confirmed. Absent on older servers (treated as confirmed). | | `error` | `string` | No | Server-side error detail when the stop was not confirmed. | ### `RunsNamespace` Use `client.runs` (`/api/v2/runs`) to poll, stream, stop, read logs, and export durable dataset rows. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `get` | `(runId: string, options?: RunsGetOptions) => Promise<PlayStatus>` | Yes | Get current run status by public run id. | | `input` | `(runId: string) => Promise<{ runId: string; input: Record<string, unknown> \| unknown[]; bytes: number; sha256: string \| null; replayedFromRunId: string \| null; }>` | Yes | Explicitly read the retained original input (may include customer data). | | `rerun` | `(runId: string, options?: RerunOptions) => Promise<RerunPlayRunResult>` | Yes | Start a fresh run from a prior run's retained input and pinned revision. | | `list` | `(options: RunsListOptions) => Promise<PlayRunListItem[]>` | Yes | List runs for one play, optionally filtered by status. | | `listPage` | `(options: RunsListOptions) => Promise<RunsListPage>` | Yes | Read one run page with total, offset, limit and completeness metadata. | | `tail` | `(runId: string, options?: RunsTailOptions) => Promise<PlayStatus>` | Yes | Stream run events and return the latest/terminal run status. | | `logs` | `(runId: string, options?: RunsLogsOptions) => Promise<RunsLogsResult>` | Yes | Fetch persisted log lines for a run. | | `exportDatasetRows` | `(input: { playName: string; tableNamespace: string; runId?: string; limit?: number; offset?: number; rowMode?: 'output' \| 'all'; }) => Promise<PlaySheetRowsResult>` | Yes | Export persisted rows for a runtime-sheet dataset/table namespace. | | `stop` | `( runId: string, options?: { reason?: string }, ) => Promise<StopPlayRunResult>` | Yes | Stop a running/waiting run. | | `stopAll` | `{ ( options: StopAllRunsOptions & { dryRun: true }, ): Promise<StopAllPlayRunsDryRunResult>; ( options?: StopAllRunsOptions & { dryRun?: false }, ): Promise<StopAllPlayRunsStopResult>; (options?: StopAllRunsOptions): Promise<StopAllPlayRunsResult>; }` | Yes | Stop active runs across the current workspace, or with `dryRun: true`<br />enumerate the exact candidates without cancelling anything. | ### `CustomerDbQueryResult` Result returned by `DeeplineClient.db.query`. Rows are intentionally untyped because the schema depends on the caller's SQL query and selected customer tables. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `scope` | `{ kind: 'database'; mutability: 'current' }` | No | This query reads the current mutable customer database, not one run snapshot. | | `command` | `string` | Yes | Database command executed by the query endpoint. | | `row_count` | `number \| null` | Yes | Total affected row count when reported by the database. | | `row_count_returned` | `number` | Yes | Number of rows included in this response. | | `truncated` | `boolean` | Yes | Whether server-side row limits truncated the result. | | `columns` | `CustomerDbColumn[]` | Yes | Column metadata for the returned rows. | | `rows` | `unknown[]` | Yes | Result rows. | -
plays-run-export-inspect-repair.md 4.7 KB
# Run, Export, Inspect, Repair Use this before scale, after every meaningful run, and whenever the user asks about billing, reruns, exports, cached rows, failed rows, logs, suspicious UI/output, or partial repair. Do not rerun to answer a billing/debug question until the existing run is inspected. ## Core Commands ```bash deepline plays run prebuilt/<name> --input '{...}' --watch deepline plays run workflow.play.ts --input '{...}' --watch deepline runs get <run-id> --full --json deepline runs logs <run-id> --out run.log --json deepline runs export <run-id> --dataset result.rows --out rows.csv deepline billing usage --limit 10 ``` ## Pilot And Scale Pilot before scale: - 1-3 rows for route shape. - 5-10 rows for hard company/contact routes. - Confirm required export columns are present. - Confirm representative non-null values or explicit `miss_reason`. - Estimate paid calls: `source rows * people/account * fallback legs`. - Prefer billing modes that charge on hits/results over attempts where coverage is uncertain. Scale only when row progress, coverage, errors, and fanout are understood. If cost is unknown or beyond pilot, explain and ask before scaling. ## Inspect A Run `runs get <run-id> --full --json` should answer: - status, run id, play reference - started/completed time - billing total credits and cap when available - result dataset handles - output preview - row count - executed/reused/failed counts when available - provider/tool failure summaries - cache/stale summary when available - suggested export commands If run-level billing is missing, say it is missing and fall back to `billing usage` only as a ledger check. ## Export Export datasets before judging output quality: ```bash deepline runs export <run-id> --dataset result.rows --out rows.csv ``` Good export output: - flat user-facing headers - nested objects flattened or projected usefully - `status`, `miss_reason`, `source`, and evidence columns - parent ids for child tables - no raw provider blobs unless requested For job-change, useful export headers include: ```text linkedin_url,current_domain,job_change.status,job_change.date,job_change.new_company,job_change.new_title,job_change.incremental_hit ``` or an approved flat equivalent. ## Billing And Cache When explaining cost, report: - run id - charged credits - billing mode and expected pricing basis from describe, if known - row count - executed/reused/failed counts when visible - cached/stale reuse explanation - whether a zero-credit run appears to be cache reuse, no billable results, or missing metadata For result-priced job change: ```text This appears to charge only on confirmed moved results. A 0-credit run can be normal if all rows reused cached work or no successful job-change event occurred. ``` Do not return credits as row output. Billing belongs in run metadata. ## Repair Classes Classify before changing route: - route mismatch - described contract mismatch - getter/output projection issue - CSV/header/row validation issue - provider/tool input issue - credentials/permission issue - infra/callback/scheduler/persistence issue - runtime/code error - UI/static-analysis/preview issue - namespace/navigation issue After two same-class failures, change branch or export partials with miss reasons. Do not loop the same paid failure. ## Partial Failures Runtime failures are acceptable when legible: - One invalid row should mark one row/cell failed when possible. - Batch-level infra failures can mark affected rows failed but must say why. - Provider 422 row validation should become row failure when possible. - Code/runtime errors can still fail the play if not row-scoped. Export partials when useful. Preserve row failure metadata: tool id, provider, failure origin, and error class. ## Suspicious UI Or Output Inspect/export before rerunning when: - grid shows `true` for a nested object - pricing disappeared from run rows - namespace navigation opens "no plays found" - static analysis shows extra/weird stages - expected nested fields are null/missing - output changed from object to boolean/string Likely fixes: - output schema or rowOutputSchema needs nested field paths - final projection is returning truthiness instead of object fields - run detail is hiding billingTotalCredits - play reference namespace is wrong (`prebuilt/<name>` vs user/org/local) - stale/cache metadata reused an old cell ## Final Response Shape When a run happened: ```text Ran <play-ref> on <N> rows. Run id: <run-id>. Result rows: <N>. Executed/reused/failed: <x>/<y>/<z> when available. Charged: <credits or unknown/missing reason>. Export: <path>. Issues: <miss/failure classes>. Next: <scale/rerun/repair/stop>. ``` When no paid run happened, say so and list the safe commands used. -
plays-sdk-reference.md 99.3 KB
# SDK Reference ## Runtime Model The Deepline SDK is a runtime SDK. Your TypeScript defines durable play code and typed run contracts; Deepline executes that code in the cloud runtime, records provider/tool calls, persists dataset rows, and exposes run state through SDK handles and HTTP APIs. Use `definePlay(...)` for code that runs inside a Deepline play. Inside that function, `ctx.*` is the runtime boundary: `ctx.tools.execute` calls managed providers, `ctx.dataset` records row-level work, `ctx.step` checkpoints scalar work, `ctx.fetch` records external HTTP, and `ctx.runPlay` composes registered or prebuilt plays. Use `Deepline.connect()` and `DeeplineClient` from regular Node/TypeScript services, scripts, schedulers, or tests. Those APIs discover tools and plays, start runs, stream/poll status, stop runs, and inspect durable output without requiring a local play file. ## Reference Map <!-- prettier-ignore --> | Area | Primary surface | Use when | |---|---|---| | Runtime entrypoint | `Deepline.connect()` / `DeeplineContext` | A script or service needs to call tools, run plays, or inspect runs. | | Play authoring | `definePlay(...)` / `ctx.*` | Code should run durably inside Deepline with persisted steps, datasets, tools, and child plays. | | Tool/provider calls | `ctx.tools.execute(...)`, `deepline.tools.execute(...)`, `client.executeTool(...)` | You need provider-backed enrichment/search with Deepline auth, billing, extraction metadata, and retries. | | Remote plays/runs | `ctx.play(name)`, `ctx.runPlay(...)`, `PlayJob`, `client.runs` | You need to run, poll, stream, stop, export, publish, or inspect plays. | | Raw HTTP | `references/plays-api-reference.md` | A backend, notebook, scheduler, or non-TypeScript caller invokes Deepline over REST. | ## Detail Policy <!-- prettier-ignore --> | Material | Rendered as | |---|---| | Tested examples | Full runnable code blocks. | | Classes | One member table with purpose, parameters, and returns. | | Interfaces and object types | Field tables; no duplicate declaration dump. | | Fieldless aliases or overloads | Compact signature line plus parameter/return tables. | | Full HTTP routes | Generated in `references/plays-api-reference.md`. | ## Tested Examples These examples are copied from `docs-examples/sdk-v2` and validated by `bun run docs:sdk-v2:check`. Keep examples there first, then regenerate this reference. ### Run A Prebuilt From TypeScript Source: `docs-examples/sdk-v2/run-prebuilt.ts` ```ts import { Deepline } from 'deepline'; const ctx = await Deepline.connect(); const job = await ctx.play('prebuilt/person-linkedin-to-email').run({ linkedin_url: 'https://www.linkedin.com/in/example-person/', }); const result = await job.get(); console.log(JSON.stringify(result, null, 2)); ``` ### Define A Play With `ctx.tools.execute` Source: `docs-examples/sdk-v2/company-lookup.play.ts` ```ts import { definePlay } from 'deepline'; type Input = { domain: string; }; type Output = { domain: string; lookupStatus: string; }; export default definePlay( 'docs-company-lookup', async (ctx, input: Input): Promise<Output> => { const result = await ctx.tools.execute({ id: 'company_lookup', tool: 'test_rate_limit', input: { key: input.domain, }, description: 'Check that the company lookup path can run for this domain.', }); return { domain: input.domain, lookupStatus: result.status, }; }, ); ``` ### Fall Through A Transient Provider Failure Catch only `ProviderTransientError` when another read provider can answer the same question. Validation, authentication, billing, Deepline, unknown, and final-provider failures stay loud. Source: `docs-examples/sdk-v2/provider-fallback.play.ts` ```ts import { definePlay, ProviderTransientError } from 'deepline'; type Input = { firstName: string; lastName: string; domain: string; }; export default definePlay( 'docs-provider-fallback', async (ctx, input: Input) => { try { const primary = await ctx.tools.execute({ id: 'primary_email', tool: 'hunter_email_finder', input: { first_name: input.firstName, last_name: input.lastName, domain: input.domain, }, description: 'Try the primary email provider.', }); return { email: primary.extractedValues.email?.get() ?? null }; } catch (error) { if (!(error instanceof ProviderTransientError)) throw error; } const fallback = await ctx.tools.execute({ id: 'fallback_email', tool: 'leadmagic_email_finder', input: { first_name: input.firstName, last_name: input.lastName, domain: input.domain, company_domain: input.domain, }, description: 'Try the fallback email provider.', }); return { email: fallback.extractedValues.email?.get() ?? null }; }, { description: 'Find an email with one safe provider-failure fallback and loud terminal errors.', }, ); ``` ### Schedule A Dataset Refresh Source: `docs-examples/sdk-v2/nightly-account-refresh.play.ts` ```ts import { definePlay } from 'deepline'; type Account = { domain: string; owner: string; }; export default definePlay( 'docs-nightly-account-refresh', async (ctx, input: { accounts: Account[]; refreshExisting?: boolean }) => { const rows = await ctx .dataset('account_refresh', input.accounts) .withColumn('company_signal', (account, rowCtx) => rowCtx.tools.execute({ id: 'company_signal', tool: 'test_rate_limit', input: { key: account.domain, }, description: 'Refresh one account signal for the owner.', staleAfterSeconds: 86_400, }), ) .run({ key: 'domain', description: 'Refresh target account signals once per day.', }); return { rows }; }, { cron: { schedule: '0 9 * * *', timezone: 'America/New_York', input: { accounts: [], refreshExisting: true }, }, billing: { maxCreditsPerRun: 25, }, }, ); ``` ### Verify A Webhook With HMAC Source: `docs-examples/sdk-v2/inbound-lead-webhook.play.ts` ```ts import { definePlay } from 'deepline'; type InboundLead = { email: string; company_domain?: string; source?: string; }; export default definePlay( 'docs-inbound-lead-webhook', async (ctx, input: InboundLead) => { const domain = input.company_domain ?? input.email.split('@')[1] ?? ''; const company = await ctx.tools.execute({ id: 'company_context', tool: 'test_rate_limit', input: { key: domain, }, description: 'Add company context before routing the inbound lead.', }); return { email: input.email, domain, source: input.source ?? 'webhook', company_status: company.status, }; }, { webhook: { auth: { type: 'standard-webhooks', headerFamily: 'standard', signingSecrets: ['INBOUND_RELAY_WEBHOOK_SECRET'], toleranceSeconds: 300, }, }, }, ); ``` ## Play Authoring Contract New artifacts pin authoring contract edition 8. Check, publish, and run use the same admitted snapshot. <!-- prettier-ignore --> | Field | Type | Required | Contract | |---|---|---:|---| | `description` | `string` | No | Optional non-empty human-readable summary of the Play. | | `compatibility.toolErrorSchemaVersion` | `0 \| 1` | No | Artifact-pinned tool error behavior, either 0 or 1. | | `compatibility.toolResponseReceiptRevision` | `string` | No | Explicit durable-receipt revision for a response transformation; bump only when serialized tool output changes. | | `inline` | `boolean` | No | Compiler hint for an inline named Play handler. | | `billing.maxCreditsPerRun` | `number` | No | Maximum Deepline credits permitted for one Play Run. | | `bindings.webhook.hmac.secretEnv` | `string` | Yes | Environment variable containing the webhook HMAC secret. | | `bindings.webhook.hmac.algorithm` | `'sha256'` | No | Webhook signature hash algorithm. Only sha256 is supported. | | `bindings.webhook.hmac.header` | `string` | No | HTTP header containing the webhook signature. | | `bindings.webhook.auth.type` | `'standard-webhooks'` | No | Uses the Standard Webhooks v1 symmetric signing scheme. | | `bindings.webhook.auth.headerFamily` | `'standard' \| 'svix'` | No | Header namespace expected from the webhook provider. | | `bindings.webhook.auth.signingSecrets[]` | `string` | No | Deepline Secret name used to verify Standard Webhooks. | | `bindings.webhook.auth.toleranceSeconds` | `number` | No | Accepted delivery timestamp skew in seconds, from 1 through 3600. | | `bindings.cron.schedule` | `string` | Yes | Five-field cron expression. | | `bindings.cron.timezone` | `string` | No | IANA timezone. Omitted means UTC. | | `bindings.cron.input` | `Record<string, unknown>` | No | Static JSON object passed to every run created by this cron binding. | | `bindings.sqlListeners` | `SqlListener[]` | No | Static provider-monitor listener declarations. | | `bindings.sqlListeners[].id` | `string` | Yes | Unique static listener identifier within one Play. | | `bindings.sqlListeners[].tool` | `string` | Yes | Modeled provider monitor tool id in provider.tool form. | | `bindings.sqlListeners[].stream` | `string` | Yes | Static output stream key exposed by the monitor tool. | | `bindings.sqlListeners[].operations[]` | `'INSERT' \| 'UPDATE' \| 'DELETE'` | No | Database operation that wakes the listener. | | `bindings.sqlListeners[].where.before` | `Record<string, SqlListenerFilterOperator>` | No | Column filters evaluated against the row before mutation. | | `bindings.sqlListeners[].where.after` | `Record<string, SqlListenerFilterOperator>` | No | Column filters evaluated against the row after mutation. | | `bindings.sqlListeners[].where.*.*.eq` | `SqlListenerFilterScalar` | No | Scalar equality condition. | | `bindings.sqlListeners[].where.*.*.neq` | `SqlListenerFilterScalar` | No | Scalar inequality condition. | | `bindings.sqlListeners[].where.*.*.in` | `readonly SqlListenerFilterScalar[]` | No | Non-empty scalar membership condition. | | `bindings.sqlListeners[].where.*.*.notIn` | `readonly SqlListenerFilterScalar[]` | No | Non-empty scalar exclusion condition. | | `bindings.sqlListeners[].where.*.*.isNull` | `true` | No | Matches null values when set to true. | | `bindings.sqlListeners[].where.*.*.isNotNull` | `true` | No | Matches non-null values when set to true. | | `bindings.sqlListeners[].where.*.*.ilike` | `string` | No | Case-insensitive SQL pattern condition. | | `bindings.secrets[]` | `string` | No | Environment variable made available to the Play. | | `staleAfterSeconds` | `number \| null` | No | `0` always executes; `null`/omitted never expires; a positive integer is a TTL in seconds. | | `ctx.tools.execute.id` | `string` | Yes | Stable durable receipt identity within one execution scope. | | `ctx.tools.execute.tool` | `K` | Yes | Integration tool id resolved against the generated ToolMap. | | `ctx.tools.execute.input` | `K extends keyof ToolMap ? ToolMap[K]['input'] : Record<string, unknown>` | Yes | Tool-specific input object. | | `ctx.tools.execute.description` | `string` | No | Human-readable purpose of the durable tool call. | | `ctx.tools.execute.force` | `boolean` | No | Explicitly bypasses a completed durable tool receipt. | | `ctx.tools.execute.timeoutMs` | `number` | No | Positive whole-number runtime transport timeout in milliseconds. | | `ctx.tools.execute.receiptWaitMs` | `number` | No | Positive whole-number durable receipt wait budget in milliseconds. | | `ctx.csv.options.description` | `string` | No | Non-empty description for a staged CSV load. | | `ctx.csv.options.columns` | `CsvRenameMap` | No | Canonical field-to-header aliases for a staged CSV. | | `ctx.csv.options.rename` | `CsvRenameMap` | No | Legacy header rename aliases for a staged CSV. | | `ctx.csv.options.required` | `readonly string[]` | No | Canonical columns required after CSV normalization. | | `ctx.dataset.key` | `string` | Yes | Stable durable identity for one dataset. | | `ctx.dataset.run.description` | `string` | No | Non-empty description for one dataset execution. | | `ctx.dataset.run.key` | `DatasetRowKey<InputRow>` | No | Stable field or fields used for durable row identity. | | `ctx.dataset.run.onRowError` | `'isolate' \| 'fail'` | No | Whether row failures isolate or fail the whole dataset. | | `ctx.dataset.run.mode` | `'upsert' \| 'net_new'` | No | Whether the dataset returns all rows or only newly admitted rows. | | `ctx.dataset.run.undrawnColumns` | `readonly string[]` | No | Computed columns deliberately left out of the authored @mermaid diagram. | | `ctx.step.id` | `string` | Yes | Stable durable identity for one scalar checkpoint. | | `ctx.step.semanticKey` | `string` | No | Optional semantic receipt identity for a scalar checkpoint. | | `ctx.step.staleAfterSeconds` | `number \| null` | No | Checkpoint freshness: null/omitted never expires, 0 always executes. | | `ctx.fetch.key` | `string` | Yes | Stable durable identity for one external HTTP request. | | `ctx.fetch.staleAfterSeconds` | `number \| null` | No | Fetch freshness: null/omitted never expires, 0 always executes. | | `ctx.runPlay.key` | `string` | Yes | Stable identity for one inline child Play call. | | `ctx.runPlay.playRef` | `string \| PlayReferenceLike` | Yes | Child Play name or typed Play definition handle. | | `ctx.runPlay.input` | `Record<string, unknown>` | Yes | Scalar input object submitted to the child Play. | | `ctx.runPlay.options.description` | `string` | Yes | Non-empty purpose for one inline child Play call. | | `ctx.runPlay.options.execution` | `'inline'` | No | Child composition strategy. Only inline is supported. | | `ctx.runPlay.options.timeoutMs` | `never` | No | Unsupported legacy child-workflow timeout. | | `ctx.runPlayAsync.key` | `string` | Yes | Stable identity for one independent child Play Run. | | `ctx.runPlayAsync.playRef` | `string \| PlayReferenceLike` | Yes | Independently published scalar child Play name or strongly typed definition handle. | | `ctx.runPlayAsync.input` | `Record<string, unknown>` | Yes | Scalar input object submitted to the independent child Run. | | `ctx.runPlayAsync.options.description` | `string` | Yes | Non-empty purpose for one independent child Play Run. | | `runtime.timeout` | `string` | No | Play-level sandbox deadline. The default is 30m; use a static duration such as 90m or 2h, up to 4h. | | `runtime.size` | `'standard'` | No | Deepline-managed sandbox size. Only standard is supported. | | `ctx.customerDb.query.statement` | `SqlQuery` | Yes | One non-empty Customer DB SQL string; the deprecated SqlQuery object is accepted only without parameter values. | | `ctx.customerDb.query.options.maxRows` | `number` | No | Positive whole-number Customer DB response row limit. | | `ctx.customerDb.query.options.timeoutMs` | `number` | No | Positive whole-number Customer DB timeout in milliseconds. | | `ctx.tool.key` | `string` | Yes | Stable receipt identity for the tool shorthand. | | `ctx.tool.tool` | `string` | Yes | Integration tool id for the tool shorthand. | | `ctx.tool.input` | `Record<string, unknown>` | Yes | Tool-specific input object for the shorthand. | | `ctx.tool.options.description` | `string` | No | Non-empty purpose for the tool shorthand. | | `ctx.runSteps.options.description` | `string` | No | Non-empty purpose for a reusable step program. | | `ctx.sleep.ms` | `number` | Yes | Non-negative whole-number sleep duration in milliseconds. | | `ctx.fetch.url` | `string` | Yes | HTTP request URL. Secret authentication requires HTTPS. | | `ctx.fetch.init.method` | `string` | No | HTTP method. Mutating methods require an Idempotency-Key. | | `ctx.fetch.init.headers.Idempotency-Key` | `string` | No | Required for mutating HTTP methods to make replay safe. | ### Durable call keys are static Durable call keys — the ctx.fetch key, the ctx.dataset key, the ctx.step id — must be static string literals. The key names a durable receipt, so check, publish, and replay have to agree on it before the body runs. A key computed at runtime cannot be resolved at check time and is rejected. This is an architectural constraint, not a style rule. A play cannot loop over a computed key, so it cannot page a large table with a helper like page(pageNumber). Unrolling one literal key per page is not a design at any real page count. Push the aggregation server-side and call it once: a SQL function, a view, or a provider endpoint that returns the whole result. Keep unrolled literal keys only for a handful of genuinely distinct calls. To fan out over rows, use ctx.dataset with a static key — the per-row receipt identity comes from the row, not from the key. ### Durable HTTP batches A static ctx.fetch key inside a loop is a warning because every iteration must still have distinct method, URL, body, or safe headers. One durable receipt must never stand in for every request. Keep the static fetch label. For a mutating batch, make the body distinct and use a replay-stable external Idempotency-Key such as `${ctx.run.id}:signals:${batchIndex}`. ### `ctx.run.id` ctx.run.id is stable while Deepline retries or resumes one durable run. A separately submitted run receives a new id. Use it when deriving an external idempotency key for a sequence of batches. ### Runtime capabilities <!-- prettier-ignore --> | Capability | Surface | Contract | |---|---|---| | Durable external I/O | `ctx.fetch`, `ctx.tools.execute`, `ctx.step`, `ctx.runPlay` | Use a `ctx.*` primitive for external work. Each primitive owns durable receipt identity and replay; raw I/O in an authored handler does not. | | Web Crypto | `crypto.subtle` | WebCrypto is available for standards-based signing, verification, encryption, and key import. Keep private material in `ctx.secrets`; `docs-examples/sdk-v2/github-app-jwt.play.ts` signs an RS256 GitHub App JWT and uses `staleAfterSeconds: 0` for its time-varying auth exchange. | | Workspace secrets | `ctx.secrets.get`, `.bearer`, `.header` | Declare the secret at the Play boundary, read it only at runtime, and never return or log it. Secret-bearing HTTP requests require HTTPS. | | Freshness | `staleAfterSeconds` on durable calls | This is call-receipt freshness: omitted/null reuses forever, zero always executes, and a positive integer is a TTL in seconds. It never changes dataset or request identity. | Generated from source comments and type declarations by `scripts/generate-play-sdk-reference.ts`. Do not edit this file manually. ## Version And Coverage <!-- prettier-ignore --> | Field | Value | |---|---| | SDK version | `0.3.90` | | SDK HTTP API | `v3` | | Checked-in SDK fallback | `0.3.90` | | Minimum supported SDK | `0.1.53` | | Deprecated below | `0.3.1` | | Generated sources | `packages/plays/authoring-contract.ts`<br />`packages/plays/cell-staleness.ts`<br />`packages/plays/dataset.ts`<br />`packages/plays/tool-execution-error.ts`<br />`packages/plays/tool-result-types.ts`<br />`packages/sdk/src/client.ts`<br />`packages/sdk/src/errors.ts`<br />`packages/sdk/src/play.ts` | | Coverage | Runtime SDK surface: `Deepline.connect`, `DeeplineContext`, `DeeplineClient`, play authoring, in-play `ctx.*` primitives, provider/tool calls, named play handles, run handles, datasets, and tool result accessors. | | Not covered | Full CLI command help, provider-specific input/output schemas, dashboard-only routes, and marketing/tutorial guides. Use `references/plays-api-reference.md` for generated HTTP route contracts. | ## Runtime Entrypoints ### `Deepline` Static entry point for the Deepline SDK. Signature: `class Deepline` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `connect` | method | Create a connected SDK context.<br /><br />Resolves configuration from options, environment variables, and CLI config<br />files. See `resolveConfig` for the resolution order. | `options?: DeeplineClientOptions` - Optional overrides for API key, base URL, etc. | `Promise<DeeplineContext>` | ### `DeeplineContext` High-level SDK context with tool shortcuts and play handles. Created by `Deepline.connect`. Wraps a `DeeplineClient` with a friendlier API for common operations. Signature: `class DeeplineContext` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Create a high-level SDK context.<br /><br />Most callers should use `Deepline.connect`; direct construction is<br />equivalent when you already have explicit client options. | `options?: DeeplineClientOptions` - Optional SDK client configuration. | | | `tools` | getter | Tool operations namespace. | | `DeeplineToolsNamespace` | | `executions` | getter | Durable state for recoverable direct tool executions. | | `DeeplineExecutionsNamespace` | | `plays` | getter | Play discovery and named-play handles.<br /><br />Use `plays.list()` for discovery and `plays.get(name)` when you prefer a<br />namespace spelling over `ctx.play(name)`. | | `DeeplinePlaysNamespace` | | `prebuilt` | getter | Convenience references for Deepline-managed prebuilt plays.<br /><br />Known prebuilts are exposed by camel-cased aliases. Any other property is<br />converted into `prebuilt/<property>` so callers can pass the reference to<br />`ctx.runPlay(...)`. | | `Record<string, PrebuiltPlayRef>` | | `play` | method | Get a named play handle for remote lifecycle operations. | `name: string` - Play name (as registered on the server) | `DeeplineNamedPlay<TInput, TOutput>` | | `runPlay` | method | Run a named or prebuilt play and wait for its output.<br /><br />This is the high-level SDK equivalent of `ctx.play(name).runSync(input)`.<br />Inside a play runtime, prefer the in-play `ctx.runPlay(key, playRef, input,<br />options)` form so the child run is checkpointed under a stable key. | `playOrRef: string \| PlayReferenceLike` - Play name or prebuilt/reference object.<br />`input: TInput` - JSON input passed to the play. | `Promise<TOutput>` | ## Play Authoring And In-Play Runtime ### `definePlay` Define a play — a composable TypeScript workflow for the Deepline platform. The returned value is both a callable function, invoked by the Deepline runtime with a runtime context, and a named play handle carrying `.run()`, `.versions()`, `.get()` and `.publish()` for remote lifecycle management. Plays are the primary abstraction for repeatable data pipelines and execute durably, with automatic retries and timeouts. Signature: `export function definePlay<TInput, TOutput extends PlayReturnObject>( config: DefinePlayConfig<TInput, TOutput>, ): DefinedPlay<TInput, TOutput>; export function definePlay< THandler extends ( context: DeeplinePlayRuntimeContext, input: any, ) => Promise<PlayReturnObject>, >( name: string, fn: THandler, bindings?: PlayBindings<NoInfer<PlayHandlerInput<THandler>>>, ): DefinedPlay<PlayHandlerInput<THandler>, PlayHandlerOutput<THandler>>;` #### Overload 1 #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `config` | `DefinePlayConfig<TInput, TOutput>` | Yes | Object-form play config. | #### Returns `DefinedPlay<TInput, TOutput>` #### Overload 2 #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `string` | Yes | Play name. | | `fn` | `THandler` | Yes | Play function. | | `bindings` | `PlayBindings<NoInfer<PlayHandlerInput<THandler>>>` | No | Play configuration, including runtime limits and triggers. | #### Returns `DefinedPlay<PlayHandlerInput<THandler>, PlayHandlerOutput<THandler>>` ### `DefinePlayConfig` Object-form play definition accepted by `definePlay(config)`. Use this form when the input contract should be explicit at definition time through `defineInput<T>(schema)`, or when configuration reads clearer as one object. The shorthand `definePlay(name, fn, bindings?)` is equivalent for simple file-backed plays.<br /> Signature: `export type DefinePlayConfig< TInput, TOutput extends PlayReturnObject, > = PlayAuthoringDefineConfig<TInput, TOutput, DeeplinePlayRuntimeContext>;` ### `PlayBindings` Optional Play configuration, including triggers and runtime limits. A play can be triggered three ways, declared as the third argument to [definePlay](/sdk-v2/sdk-reference#defineplay): - `webhook` — an inbound HTTP call (with optional legacy HMAC or Standard Webhooks signature verification); - `cron` — a schedule; or - `sqlListeners` — a **monitor**: the play runs whenever a monitor writes a new row to its output stream. This is how you build a play "on top of" a monitor (e.g. run enrichment every time a watched company posts a new job). Each listener binds to a monitor tool id + one of its output stream keys (see `deepline monitors available <id>` for a tool's streams and row columns). The changed row is delivered to the handler as the listener event's `after`. The default Play runtime is 30 minutes. For bounded long-running batches, add `runtime: { timeout: '90m', size: 'standard' }`; duration values are whole minutes or hours, up to `4h`. It differs from `ctx.tools.execute({ timeoutMs })`, which limits one provider call.<br /> Signature: `export type PlayBindings<TInput = Record<string, unknown>> = PlayAuthoringBindings<TInput>;` ### `ctx.csv(path, options)` Load a staged CSV file as a durable dataset handle. Signature: `csv<T = Record<string, unknown>>( path: string | CsvInput<T & object>, options?: CsvOptions, ): Promise<PlayDataset<T>>;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `path` | `string \| CsvInput<T & object>` | Yes | | | `options` | [`CsvOptions`](#csvoptions) | No | | #### Returns `Promise<PlayDataset<T>>` — see [`PlayDataset`](#playdataset) ### `CsvOptions` Options for loading a staged CSV with `ctx.csv(...)`.<br /> Signature: `export type CsvOptions = CsvOptions;` ### `ctx.dataset(key, items)` Create a persisted row dataset and define durable output columns. Signature: `dataset<TSource extends PlayDatasetInput<object>>( key: string, items: TSource, ): DatasetBuilder< PlayDatasetRow<TSource> & object, PlayDatasetRow<TSource> & object, PlayAuthoringRuntimeContext >;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `key` | `string` | Yes | | | `items` | `TSource` | Yes | | #### Returns `DatasetBuilder< PlayDatasetRow<TSource> & object, PlayDatasetRow<TSource> & object, PlayAuthoringRuntimeContext >` ### `.dataset(...).withColumn(name, resolver).run(options)` Define one output column for every row in this dataset. ```ts withColumn<Name extends string, Value>( name: Name, resolver: ColumnResolver<OutputRow, Value>, ): DatasetBuilder< InputRow, OutputRow & Record<Name, Value> >; withColumn<Name extends string, Value>( name: Name, definition: DatasetColumnDefinition< OutputRow, Value > & { readonly runIf: ( row: OutputRow, index: number, ) => boolean | Promise<boolean>; }, ): DatasetBuilder< InputRow, OutputRow & Record<Name, Value | null> >; withColumn<Name extends string, Value>( name: Name, definition: DatasetColumnDefinition< OutputRow, Value >, ): DatasetBuilder< InputRow, OutputRow & Record<Name, Value> >; withColumn<Name extends string, Value>( name: Name, resolver: | StepResolver<OutputRow, Value> | RunnableStepProgram<unknown, Value>, options: StepOptions<OutputRow, Value>, ): DatasetBuilder< InputRow, OutputRow & Record<Name, Value | null> >; run( options?: DatasetRunOptions<InputRow>, ): Promise<PlayDataset<OutputRow>>; ``` #### Column Overload 1 Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `Name` | Yes | | | `resolver` | `ColumnResolver<OutputRow, Value>` | Yes | | #### Column Overload 1 Returns `DatasetBuilder< InputRow, OutputRow & Record<Name, Value> >` #### Column Overload 2 Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `Name` | Yes | | | `definition` | `DatasetColumnDefinition< OutputRow, Value > & { readonly runIf: ( row: OutputRow, index: number, ) => boolean \| Promise<boolean>; }` | Yes | | #### Column Overload 2 Returns `DatasetBuilder< InputRow, OutputRow & Record<Name, Value | null> >` #### Column Overload 3 Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `Name` | Yes | | | `definition` | `DatasetColumnDefinition< OutputRow, Value >` | Yes | | #### Column Overload 3 Returns `DatasetBuilder< InputRow, OutputRow & Record<Name, Value> >` #### Column Overload 4 Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `Name` | Yes | | | `resolver` | `\| StepResolver<OutputRow, Value> \| RunnableStepProgram<unknown, Value>` | Yes | | | `options` | `StepOptions<OutputRow, Value>` | Yes | | #### Column Overload 4 Returns `DatasetBuilder< InputRow, OutputRow & Record<Name, Value | null> >` #### Run Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `options` | `DatasetRunOptions<InputRow>` | No | | #### Run Returns `Promise<PlayDataset<OutputRow>>` — see [`PlayDataset`](#playdataset) Execute the row-column program and return a durable dataset handle. `upsert` preserves row-by-row enrichment. `net_new` admits and returns only unseen stable keys. `isolate` records failed rows while siblings continue; `fail` opts into fail-fast behavior. ### `DatasetColumnRunInput` Input object passed to an object-column `run` resolver. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `row` | `Row` | Yes | Current row, including previously computed columns. | | `ctx` | `DeeplinePlayRuntimeContext` | Yes | Runtime context for tool, Play, fetch, and log calls. | | `index` | `number` | Yes | Zero-based row index for this dataset run. | | `previousCell` | `PreviousCell<Value>` | No | Prior stored cell value and freshness metadata when this cell reruns. | ### `DatasetColumnDefinition` Object-column form for `.withColumn(...)`. Use this when a column needs `runIf` or typed `previousCell`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `run` | `( input: DatasetColumnRunInput<Row, Value>, ) => Value \| Promise<Value>` | Yes | Compute one cell value. Receives the previous stored value when rerunning. | | `runIf` | `(row: Row, index: number) => boolean \| Promise<boolean>` | No | Optional row-level gate. Skipped rows produce `null` for this column. | ### `StepOptions` Options for row-level `.withColumn(...)` and `steps().step(...)` entries. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `runIf` | `(row: Row, index: number) => boolean \| Promise<boolean>` | No | Optional row-level gate. Skipped rows produce `null` for this column. | | `recompute` | `boolean` | No | Legacy dataset-column flag. Prefer freshness on the reusable call. | | `recomputeOnError` | `boolean` | No | Legacy error-recompute flag accepted for older authored Plays. | | `staleAfterSeconds` | `number` | No | Legacy cell staleness metadata accepted for older authored Plays. | ### `PreviousCell` Previous durable cell value passed to object-column resolvers. The runtime supplies this when a row+column is being recomputed after a previous value existed. `value` has the same type that the column returns; freshness metadata lives beside it. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `value` | `Value` | Yes | Previous completed value for this row+column. | | `completedAt` | `number` | No | Millisecond timestamp when the previous value completed. | | `staleAt` | `number \| null` | No | Millisecond timestamp when the previous value becomes stale; `null` means no expiry. | | `staleAfterSeconds` | `number` | No | Resolved numeric TTL in seconds for the previous value, when present. | ### `ctx.step(id, fn)` Create one scalar durable checkpoint. Signature: `step<T>( id: string, run: () => T | Promise<T>, options?: RuntimeStepOptions, ): Promise<T>;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `id` | `string` | Yes | | | `run` | `() => T \| Promise<T>` | Yes | | | `options` | `RuntimeStepOptions` | No | | #### Returns `Promise<T>` ### `ctx.runPlay(key, playRef, input, options)` Compose another Play inline under a stable call key. Signature: `runPlay<TOutput = unknown>( key: string, playRef: string | PlayReferenceLike, input: Record<string, unknown>, options: PlayCallOptions, ): Promise<TOutput>;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `key` | `string` | Yes | | | `playRef` | `string \| PlayReferenceLike` | Yes | | | `input` | `Record<string, unknown>` | Yes | | | `options` | `PlayCallOptions` | Yes | | #### Returns `Promise<TOutput>` ### `ctx.runPlayAsync(key, playRef, input, options)` Admit an independent scalar Play Run and return its durable identity after scheduler admission, before child completion. Signature: `runPlayAsync<TInput extends Record<string, unknown>>( key: string, playRef: PlayReferenceLike<TInput>, input: NoInfer<TInput>, options: PlayAuthoringAsyncCallOptions, ): Promise<AsyncPlayRunRef>;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `key` | `string` | Yes | | | `playRef` | `PlayReferenceLike<TInput>` | Yes | | | `input` | `NoInfer<TInput>` | Yes | | | `options` | `PlayAuthoringAsyncCallOptions` | Yes | | #### Returns `Promise<AsyncPlayRunRef>` — see [`AsyncPlayRunRef`](#asyncplayrunref) ### `AsyncPlayRunRef` Serializable reference returned after an async child Run is durable.<br /> Signature: `export type AsyncPlayRunRef = Readonly<{ runId: PlayRunId; }>;` ### `ctx.tools.execute(request)` Execute a provider tool through the terminal-result cache contract. Signature: `execute<TOutput = PlayLooseObject>( request: PlayToolExecutionRequest, ): Promise<ToolExecuteResult<TOutput>>;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `request` | `PlayToolExecutionRequest` | Yes | | #### Returns `Promise<ToolExecuteResult<TOutput>>` — see [`ToolExecuteResult`](#toolexecuteresult) ### `ToolExecutionRequest` Keyword-style request object for `ctx.tools.execute(...)`. The `tool` value comes from live tool discovery. The `id` is the stable logical call name used for logs, metadata, and result-cache identity. Provider result reuse is keyed by play, tool, semantic input, auth scope, provider action version, and cache policy.<br /> Signature: `export type ToolExecutionRequest = PlayToolExecutionRequest;` ### `ctx.fetch(key, url, init)` Execute a guarded HTTP request. By default it is durable and replay-safe; `staleAfterSeconds` governs only that completed call receipt, never dataset/request identity. Pass `{ transient: true }` for short-lived credential exchanges or other response data that must not be retained in a receipt or checkpoint. Edition 5+ throws `CtxFetchHttpError` for non-2xx; catch it only when the Play intentionally recovers, otherwise let it fail the Play. Editions 1–4 retain their previous response projections. Signature: `fetch( key: string, url: string | URL, init?: SecretAwareRequestInit, options?: FetchOptions, ): Promise<PlayFetchResponse>;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `key` | `string` | Yes | | | `url` | `string \| URL` | Yes | | | `init` | [`SecretAwareRequestInit`](#secretawarerequestinit) | No | | | `options` | `FetchOptions` | No | | #### Returns `Promise<PlayFetchResponse>` — see [`PlayFetchResponse`](#playfetchresponse) ### `ctx.secrets.get(name)` Read an allowed workspace secret inside the running Play; do not log or return it. Declare uppercase names in top-level `secrets`. Signature: `get(name: string): PlaySecretPromise;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `string` | Yes | | #### Returns `PlaySecretPromise` ### `ctx.secrets.bearer(secret)` Send a credential as `Authorization: Bearer <value>`. Await `get` first; its direct promise remains accepted for source compatibility, while other promises are rejected. Signature: `bearer( secret: string | PlaySecretPromise | SecretHandle, ): SecretAuth;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `secret` | `string \| PlaySecretPromise \| SecretHandle` | Yes | | #### Returns `SecretAuth` — see [`SecretAuth`](#secretauth) ### `ctx.secrets.header(header, secret)` Send a credential as a named header, for APIs that do not use bearer tokens — `x-api-key`, `apikey`, `private-token`, and similar. Signature: `header( header: string, secret: string | PlaySecretPromise | SecretHandle, ): SecretAuth;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `header` | `string` | Yes | | | `secret` | `string \| PlaySecretPromise \| SecretHandle` | Yes | | #### Returns `SecretAuth` — see [`SecretAuth`](#secretauth) ### `SecretAwareRequestInit` The `init` accepted by `ctx.fetch`. Same shape as `RequestInit` plus `auth`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `headers` | `HeadersInit` | No | Ordinary request headers, recorded in the durable receipt with any resolved Play secret value redacted. Prefer `auth` for credentials: it enforces HTTPS and keeps the auth header out of the receipt. | | `auth` | `SecretAuthInput` | No | One or more credentialed headers for this request. Pass a single `ctx.secrets` auth for the common case, or an array when an API requires multiple credentialed headers. Auth-helper requests require HTTPS and omit the credential from the durable receipt. Each auth entry must target a distinct header. | ### `PlayFetchResponse` A durable response record, not a WHATWG `Response`: read the already-materialized `bodyText` and `json` properties; do not call `.json()`, `.text()`, or `.body`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `ok` | `boolean` | Yes | True when the response status is in the 2xx range. | | `status` | `number` | Yes | HTTP status code as returned by the upstream server. | | `statusText` | `string` | Yes | HTTP status text as returned by the upstream server. | | `url` | `string` | Yes | Final response URL after any redirects. | | `headers` | `Record<string, string>` | Yes | Response headers, lowercased, with any known secret values redacted. | | `bodyText` | `string` | Yes | Full response body as text, with any known secret values redacted. | | `json` | `unknown \| null` | Yes | The parsed body, eagerly decoded at request time. Read it as a property — `const body = res.json`, never `await res.json()`. Null when the body is empty AND when it is not valid JSON: a malformed payload is reported as null rather than thrown, so check `res.ok` and fall back to `res.bodyText` before treating null as an empty result. | ### `SecretHandle` An opaque reference to a workspace secret used by legacy authoring-contract editions. New Plays receive plaintext strings from `ctx.secrets.get`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `string` | Yes | Name of the workspace secret, uppercased. Never its value. | ### `SecretAuth` One resolved authentication scheme, built by `ctx.secrets.bearer` or `ctx.secrets.header` and attached to a request through `init.auth`. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `kind` | `'bearer' \| 'header'` | Yes | `bearer` sends `Authorization: Bearer <value>`; `header` sends a named header. | | `secret` | `string \| PlaySecretPromise \| PlaySecretValue` | Yes | The value whose bytes the runtime attaches. | | `header` | `string` | No | Header name, set only when `kind` is `header`. | ### `CtxFetchHttpError` Edition 5+ `ctx.fetch` error for a non-2xx response. Its full readable body is secret-redacted; editions 1–4 retain `PlayFetchResponse { ok: false }`. Signature: `class CtxFetchHttpError extends Error` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Build the error from the durable response record. | `response: PlayAuthoringFetchResponse` | | | `code` | property | Stable machine-readable HTTP-failure code. | | `"CTX_FETCH_HTTP_ERROR"` | | `status` | property | Upstream HTTP status. | | `number` | | `statusText` | property | Upstream HTTP status text. | | `string` | | `url` | property | Final response URL after redirects. | | `string` | | `headers` | property | Secret-redacted response headers. | | `Record<string, string>` | | `bodyText` | property | Complete secret-redacted response body. | | `string` | | `json` | property | Eagerly parsed secret-redacted JSON, or null. | | `unknown \| null` | ### `ctx.runSteps(program, input, options)` Execute one reusable step program against a scalar input. Signature: `runSteps<TInput extends Record<string, unknown>, TOutput>( program: PlayAuthoringRunnableStepProgram< TOutput, PlayAuthoringRuntimeContext > & { readonly __inputType?: (input: TInput) => void }, input: TInput, options?: PlayAuthoringRunStepsOptions, ): Promise<TOutput>;` #### Parameters <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `program` | `PlayAuthoringRunnableStepProgram< TOutput, PlayAuthoringRuntimeContext > & { readonly __inputType?: (input: TInput) => void }` | Yes | | | `input` | `TInput` | Yes | | | `options` | `PlayAuthoringRunStepsOptions` | No | | #### Returns `Promise<TOutput>` ### `PlayDataset` Durable handle for rows produced by `ctx.csv(...)` or `ctx.dataset(...).run()`. A `PlayDataset` is not a normal in-memory array. It points at runtime-managed rows, usually backed by persisted sheet storage, and carries metadata such as dataset kind, dataset id, table namespace, count, and preview rows. Pass dataset handles directly into later `ctx.dataset(...)` stages by default so Deepline keeps row progress, retries, memory use, and table output under runtime control. Use `count()` and `peek()` for bounded inspection. Use `materialize(limit)` or async iteration only when the dataset is intentionally small and bounded. `PlayDataset` intentionally does not expose `.rows`, `.toArray()`, `.length`, numeric indexing, spread, or synchronous iteration; those hide the runtime cost of loading persisted rows into memory or make behavior depend on whether rows happen to be resident. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `datasetKind` | `PlayDatasetKind` | Yes | Dataset kind. | | `datasetId` | `string` | Yes | Dataset id. | | `backing` | `PlayDatasetBacking` | No | Backing store info. | | `sourceLabel` | `string \| null` | No | Display label. | | `tableNamespace` | `string \| null` | No | Runtime table name. | ### `ToolExecuteResult` Canonical result returned by Deepline tool execution. The top-level object is Deepline-owned execution metadata and semantic extraction state. The canonical provider response lives under `toolResponse.rawV2`; `toolResponse.raw` remains the legacy compatibility projection. Response metadata lives under `toolResponse.meta`. Semantic single-value getters live under `extractedValues.<name>.get()`, and list getters live under `extractedLists.<name>.get()`. Use extractors first when a tool contract exposes them. Use list getters for row-shaped data. Drop to `toolResponse.raw` only for provider-specific scalar fields or bounded debugging context; persisted rows may clip declared lists to previews.<br /> Signature: `export type ToolExecuteResult< TResult = unknown, TMeta = Record<string, unknown>, TExtracted extends Record<string, unknown> = Partial<DeeplineGetterValueMap>, TLists extends Record<string, Record<string, unknown>> = Record< string, Record<string, unknown> >, > = ToolExecuteResultBase<TResult, TMeta> & ToolExecuteResultAccessors<TExtracted, TLists>;` ## Errors And Provider Fallthrough New Plays receive typed tool errors. Existing published artifacts keep the error contract stored with their revision. For a read waterfall, catch only `ProviderTransientError` and keep the final provider call loud. For structured diagnostics, narrow to `ToolExecutionError` and branch on its stable fields. Never branch on `error.message`. A newly authored Play can explicitly retain legacy errors with `compatibility: { toolErrorSchemaVersion: 0 }` in its `definePlay` options. Use that only while migrating old message-based handling. ### `DeeplineError` Base error class shared by the SDK and play runtime. The global brand preserves `instanceof DeeplineError` when a bundled play and the runtime load separate physical copies of this module. Signature: `class DeeplineError extends Error` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Construct a Deepline error.<br /><br />SDK and runtime code construct these errors. Application and Play code<br />normally catches the public subclasses instead. | `message: string` - Human-readable failure summary.<br />`statusCode?: number` - HTTP status when one exists.<br />`code?: string` - Stable machine-readable code when one exists.<br />`details?: Record<string, unknown>` - Local diagnostic context; never a portable error contract. | | | `statusCode` | property | HTTP status when the failure crossed an HTTP boundary. | | `number` | | `code` | property | Stable machine-readable error code when one exists. | | `string` | | `details` | property | Local diagnostic context; not a portable error contract. | | `Record<string, unknown>` | ### `ToolExecutionErrorOrigin` The boundary responsible for a failed tool call. Use `provider` to distinguish a provider answer from caller input and Deepline infrastructure. `unknown` fails closed and must not trigger a waterfall fallback.<br /> Signature: `export type ToolExecutionErrorOrigin = | 'caller' | 'provider' | 'deepline' | 'unknown';` ### `ToolExecutionErrorCategory` The stable reason family for a failed tool call. Branch on this field only after narrowing to `ToolExecutionError`. Catch `ProviderTransientError` when the policy is simply “try the next read provider”; it is the safer and shorter waterfall contract.<br /> Signature: `export type ToolExecutionErrorCategory = | 'validation' | 'authentication' | 'authorization' | 'rate_limit' | 'network' | 'upstream' | 'billing' | 'conflict' | 'internal' | 'unknown';` ### `ToolExecutionNetworkKind` The transport failure observed when `category` is `network`. This is `null` for failures that are not network failures.<br /> Signature: `export type ToolExecutionNetworkKind = | 'timeout' | 'dns' | 'connect' | 'reset' | 'unavailable' | 'unknown';` ### `ToolExecutionNetworkScope` The request boundary on which a network failure occurred. `deepline_to_provider` is provider-side. Client and runtime scopes are Deepline transport failures and never qualify as provider fallthrough.<br /> Signature: `export type ToolExecutionNetworkScope = | 'client_to_deepline' | 'runtime_to_deepline' | 'deepline_to_provider';` ### `ProviderTransientErrorCategory` Provider-owned failure categories that may fall through to another read provider.<br /> Signature: `export type ProviderTransientErrorCategory = | 'rate_limit' | 'network' | 'upstream';` ### `ToolExecutionPublicDetails` Bounded, primitive-only diagnostics explicitly approved for customers. Raw provider bodies, credentials, prompts, stacks, and causes never belong in this shared API/SDK/Play contract.<br /> Signature: `export type ToolExecutionPublicDetails = Readonly< Record<string, string | number | boolean | null> >;` ### `ToolExecutionFailureV1` Portable version-1 `tool_error` payload. This allowlisted shape crosses the API, runtime, and SDK boundaries. `message` remains on the Error object and is deliberately not a policy field. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `schemaVersion` | `typeof TOOL_EXECUTION_ERROR_SCHEMA_VERSION` | Yes | Payload version. | | `toolId` | `string` | Yes | Public tool id passed to `tools.execute`. | | `provider` | `string \| null` | Yes | Provider responsible for the operation, or `null`. | | `operation` | `string \| null` | Yes | Provider operation name, or `null`. | | `code` | `string \| null` | Yes | Stable machine-readable failure code, or `null`. | | `origin` | `ToolExecutionErrorOrigin` | Yes | Boundary responsible for the failure. | | `category` | `ToolExecutionErrorCategory` | Yes | Stable reason family. | | `retryable` | `boolean` | Yes | Whether repeating the same semantic call is delivery-safe. | | `statusCode` | `number \| null` | Yes | HTTP status when one exists, or `null`. | | `requestId` | `string \| null` | Yes | Provider or Deepline request id, or `null`. | | `retryAfterMs` | `number \| null` | Yes | Suggested same-call retry delay in milliseconds, or `null`. | | `networkKind` | `ToolExecutionNetworkKind \| null` | Yes | Network failure kind, or `null`. | | `networkScope` | `ToolExecutionNetworkScope \| null` | Yes | Network boundary that failed, or `null`. | | `publicDetails` | `ToolExecutionPublicDetails \| null` | No | Explicitly allowlisted customer diagnostics, when present. | ### `ToolExecutionErrorOptions` Constructor input for a structured tool failure. Deepline creates these values while decoding the versioned wire payload. Customer code normally reads `ToolExecutionError` fields instead of constructing an error. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `details` | `Record<string, unknown>` | No | Local diagnostic context inherited from DeeplineError. This is not part of<br />the portable failure payload and is intentionally omitted by serialization. | ### `ToolExecutionError` A failed `tools.execute` call with stable, allowlisted provenance. `retryable` means Deepline's delivery/idempotency contract says it is safe to repeat the same semantic call. It does not describe durable receipt repairability and does not make arbitrary side-effecting fallbacks safe. In a Play, catch `ProviderTransientError` to continue a read waterfall and let every other `ToolExecutionError` remain loud. In an SDK client, catch this base class when you need structured diagnostics for every tool failure. Signature: `class ToolExecutionError extends DeeplineError` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Construct a structured tool error.<br /><br />Deepline constructs this from the versioned `tool_error` payload.<br />Application and Play code should catch it rather than create it. | `message: string`<br />`options: ToolExecutionErrorOptions` | | | `toolId` | property | Public tool id passed to `tools.execute`. | | `string` | | `provider` | property | Provider responsible for the operation, or `null` when unattributed. | | `string \| null` | | `operation` | property | Provider operation name, or `null` when unavailable. | | `string \| null` | | `origin` | property | Boundary responsible for the failure. | | `ToolExecutionErrorOrigin` | | `category` | property | Stable reason family for policy and diagnostics. | | `ToolExecutionErrorCategory` | | `retryable` | property | Whether repeating the same semantic call is delivery-safe.<br /><br />This does not mean the error may be ignored. Waterfall fallthrough is<br />represented by `ProviderTransientError`. | | `boolean` | | `requestId` | property | Provider or Deepline request id, or `null` when unavailable. | | `string \| null` | | `retryAfterMs` | property | Suggested same-call retry delay in milliseconds, or `null`. | | `number \| null` | | `networkKind` | property | Network failure kind, or `null` for non-network failures. | | `ToolExecutionNetworkKind \| null` | | `networkScope` | property | Network boundary that failed, or `null` for non-network failures. | | `ToolExecutionNetworkScope \| null` | | `publicDetails` | property | Explicitly allowlisted diagnostics safe for SDK and Play callers. | | `ToolExecutionPublicDetails \| null` | ### `ProviderTransientError` A provider-owned transient failure that is safe to handle as an empty waterfall leg. Validation, auth, billing, Deepline, and unknown failures never satisfy this type. `retryable` remains independent: it says whether the same semantic call may be repeated safely. Falling through to a different read provider depends on this class, not on `retryable`. Signature: `class ProviderTransientError extends ToolExecutionError` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Constructed by Deepline when a provider-owned transient failure arrives. | `message: string`<br />`options: Omit<ToolExecutionErrorOptions, 'origin' \| 'category'> & { category: ProviderTransientErrorCategory; }` | | | `origin` | property | Provider attribution is guaranteed for this subtype. | | `"provider"` | | `category` | property | Provider failure category that made this error eligible for fallthrough. | | `ProviderTransientErrorCategory` | ### `AuthError` Thrown when the API rejects the request due to an invalid or missing API key. This maps to HTTP 401 responses. HTTP 403 means the caller was authenticated but lacks permission, so the SDK preserves the server's API error instead. The SDK never retries auth errors — they fail immediately. Fix: run `deepline auth register` to obtain a valid key, or pass one via the `apiKey` option or `DEEPLINE_API_KEY` environment variable. Signature: `class AuthError extends DeeplineError` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Constructed by the SDK when Deepline rejects the caller's credentials. | `message?: string` | | ### `RateLimitError` Thrown when the API returns HTTP 429 (Too Many Requests). The SDK retries rate-limited requests automatically up to `maxRetries` times with exponential backoff. This error is only thrown when all retries are exhausted. Use `RateLimitError.retryAfterMs` to implement your own backoff if needed. Signature: `class RateLimitError extends DeeplineError` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Constructed by the SDK after exhausting HTTP-level rate-limit retries. | `retryAfterMs?: number`<br />`message?: string` | | | `retryAfterMs` | property | Milliseconds to wait before retrying, from the `Retry-After` response header. Defaults to 5000. | | `number` | ### `ToolRateLimitError` Tool-specific 429 preserving both historical RateLimitError catches and the structured ToolExecutionError ontology. JavaScript has one prototype chain, so this class extends RateLimitError and carries ToolExecutionError's stable cross-bundle brand. This class appears in external SDK calls after HTTP 429 retries are exhausted. It also satisfies `instanceof ToolExecutionError` and, for a provider-owned rate limit, `instanceof ProviderTransientError`. Authored Plays should use `ProviderTransientError`; they do not need this compatibility class. Signature: `class ToolRateLimitError extends RateLimitError` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Constructed by the SDK after a structured tool HTTP 429. | `message: string`<br />`options: ToolExecutionErrorOptions` | | | `toolId` | property | Public tool id passed to `tools.execute`. | | `string` | | `provider` | property | Provider responsible for the operation, or `null`. | | `string \| null` | | `operation` | property | Provider operation name, or `null`. | | `string \| null` | | `code` | property | Stable machine-readable failure code when one exists. | | `string \| undefined` | | `origin` | property | Boundary responsible for the failure. | | `ToolExecutionError['origin']` | | `category` | property | Stable reason family for policy and diagnostics. | | `ToolExecutionError['category']` | | `retryable` | property | Whether repeating the same semantic call is delivery-safe. | | `boolean` | | `requestId` | property | Provider or Deepline request id, or `null`. | | `string \| null` | | `networkKind` | property | Network failure kind, or `null` for non-network failures. | | `ToolExecutionError['networkKind']` | | `networkScope` | property | Network boundary that failed, or `null` for non-network failures. | | `ToolExecutionError['networkScope']` | | `publicDetails` | property | Explicitly allowlisted diagnostics safe for SDK callers. | | `ToolExecutionError['publicDetails']` | ### `ConfigError` Thrown when the SDK cannot resolve a valid configuration. Most commonly: no API key found in any of the resolution sources (explicit option, environment variable, CLI env files). Signature: `class ConfigError extends DeeplineError` #### Members <!-- prettier-ignore --> | Member | Kind | Purpose | Parameters | Returns / type | |---|---|---|---|---| | `constructor` | constructor | Construct a local SDK configuration failure. | `message: string` | | ## Tool And Provider Calls ### `DeeplineContext.tools` Tool/provider operations available from a connected `DeeplineContext`. This namespace is for regular SDK callers outside a play runtime. Inside a `definePlay(...)` body, use `ctx.tools.execute({ id, tool, input, ... })` so provider calls become durable runtime checkpoints.<br /> Signature: `export type DeeplineToolsNamespace = { list(): Promise<ToolDefinition[]>; get(toolId: string): Promise<ToolMetadata>; execute( toolId: string, input: Record<string, unknown>, options?: { idempotencyKey?: string; recover?: boolean; onExecution?: (execution: { idempotencyKey: string; }) => void | Promise<void>; recoveryTimeoutMs?: number; }, ): Promise<DeeplineToolExecuteResult>; };` ## Remote Plays And Runs ### `DeeplineContext.plays` Named-play discovery and handle operations from a connected `DeeplineContext`.<br /> Signature: `export type DeeplinePlaysNamespace = { list(): Promise<PlayListItem[]>; get<TInput = Record<string, unknown>, TOutput = unknown>( name: string, ): DeeplineNamedPlay<TInput, TOutput>; };` ### `DeeplineNamedPlay` Handle to a named play for remote lifecycle operations. Returned by `DeeplineContext.play` and attached to `DefinedPlay`. Provides methods to run, inspect, list runs, and publish a play by name. <!-- prettier-ignore --> | Name | Type | Required | Description | |---|---|---:|---| | `name` | `string` | Yes | The play's name. | ### `PlayJob` Handle to a running play execution. Provides methods to check status, stream logs, wait for completion, or cancel the execution. This handle is the SDK-context equivalent of `deepline plays run --watch` and `POST /api/v2/plays/run`: every surface retur
-
-
scripts
-
clay-extract-bookmarklet.js 10.5 KB
/* * Clay Table Extractor — Bookmarklet (source) * * Run this on a Clay table view URL, e.g. * https://app.clay.com/workspaces/1114258/tables/t_xxx/views/gv_xxx * or on a workbook URL, e.g. * https://app.clay.com/workspaces/941989/workbooks/wb_xxx * On a workbook it enumerates every child table via * GET /v3/workbooks/{WORKBOOK_ID}/tables * and extracts each one, emitting { _meta, workbook, tables: [ <extract>, ... ] }. * * It uses the already-authenticated browser session (credentials: 'include', * so the claysession cookie is sent automatically) to pull, from Clay's * internal v3 API: * - table config GET /v3/tables/{TABLE_ID} (fields, typeSettings, prompts, action bindings) * - schema + samples GET /v3/tables/{TABLE_ID}/views/{VIEW_ID}/table-schema-v2 * - all record ids GET /v3/tables/{TABLE_ID}/views/{VIEW_ID}/records/ids * - full cell data POST /v3/tables/{TABLE_ID}/bulk-fetch-records (batched) * * Output: downloads a single JSON file shaped to match the clay-extract.py * extract the deepline clay-to-deepline skill expects: * { _meta, table, fields, tableSchema, exampleRecords, recordIds, bulkFetchRecords } * * Two ways to run it: * 1. Human: minify to a `javascript:` URL (see clay-extract-bookmarklet.url.txt) * and drag it to the bookmarks bar, then click it on a Clay table tab. * 2. Agent (Claude-in-Chrome): paste this IIFE body into `javascript_tool` on * the table tab. It assigns the result to `window.__clayExtract`; read that * (or return JSON.stringify) instead of relying on the file download. * * The cookie is NEVER read, logged, or written — only the browser uses it. */ (async () => { const API = 'https://api.clay.com'; const MAX_RECORDS = 500; // cap full pull; raise if you need every row const BATCH = 50; // bulk-fetch page size const path = location.pathname; const tableId = (path.match(/\/tables\/(t_[A-Za-z0-9]+)/) || [])[1]; const workbookId = (path.match(/\/workbooks\/(wb_[A-Za-z0-9]+)/) || [])[1]; const urlViewId = (path.match(/\/views\/(gv_[A-Za-z0-9]+)/) || [])[1]; if (!tableId && !workbookId) { alert( 'Clay extract: no table or workbook id in this URL.\n\n' + 'Open a table view (/tables/t_.../views/gv_...) or a workbook ' + '(/workbooks/wb_...) and run this again.', ); return; } const toast = (msg) => { let el = document.getElementById('__clay_extract_toast'); if (!el) { el = document.createElement('div'); el.id = '__clay_extract_toast'; el.style.cssText = 'position:fixed;z-index:2147483647;bottom:20px;right:20px;background:#111;color:#fff;' + 'font:13px/1.4 -apple-system,system-ui,sans-serif;padding:10px 14px;border-radius:8px;' + 'box-shadow:0 4px 16px rgba(0,0,0,.3);max-width:320px'; document.body.appendChild(el); } el.textContent = 'Clay extract: ' + msg; return el; }; const getJSON = async (url, opts) => { const r = await fetch(url, { credentials: 'include', headers: { accept: 'application/json, text/plain, */*', ...(opts && opts.headers) }, ...opts, }); if (!r.ok) throw new Error(url.replace(API, '') + ' → ' + r.status); return r.json(); }; // Extract one table. `label` prefixes toasts so workbook runs show progress. const extractTable = async (tableId, preferredViewId, label) => { const tag = label ? label + ' ' : ''; toast(tag + 'fetching table config…'); const table = await getJSON(API + '/v3/tables/' + tableId); // Resolve view id from the table if it wasn't in the URL. const viewId = preferredViewId || table?.table?.firstViewId || table?.firstViewId; if (!viewId) throw new Error('could not resolve a view id (firstViewId missing)'); // table-schema-v2 returns { tableSchema: { f_xxx: {...} }, exampleRecords: [ { f_xxx: <rendered value> } ] } // exampleRecords here carry RENDERED formula/action cell values (richest source) — capped by Clay (~2-66 rows). toast(tag + 'fetching schema + sample records…'); let schemaTree = null; let exampleRecords = []; try { const sv2 = await getJSON( API + '/v3/tables/' + tableId + '/views/' + viewId + '/table-schema-v2', ); schemaTree = sv2?.tableSchema || sv2 || null; exampleRecords = sv2?.exampleRecords || sv2?.records || sv2?.sampleRecords || []; } catch (e) { console.warn('[clay-extract] table-schema-v2 failed:', e.message); } // True record count (no pagination on records/ids, but this confirms the cap). let totalRecordCount = null; try { const cnt = await getJSON(API + '/v3/tables/' + tableId + '/count'); totalRecordCount = cnt?.tableTotalRecordsCount ?? cnt?.count ?? null; } catch (e) { console.warn('[clay-extract] count failed:', e.message); } toast(tag + 'fetching record ids…'); let recordIds = []; try { const ids = await getJSON( API + '/v3/tables/' + tableId + '/views/' + viewId + '/records/ids', ); // Clay returns { results: [r_xxx, ...] }; tolerate older shapes too. recordIds = ids?.results || ids?.recordIds || ids?.ids || (Array.isArray(ids) ? ids : []); } catch (e) { console.warn('[clay-extract] records/ids failed:', e.message); } const truncated = recordIds.length > MAX_RECORDS; const idsToFetch = recordIds.slice(0, MAX_RECORDS); const bulkFetchRecords = []; for (let i = 0; i < idsToFetch.length; i += BATCH) { const chunk = idsToFetch.slice(i, i + BATCH); toast(tag + 'records ' + (i + chunk.length) + '/' + idsToFetch.length + '…'); try { const res = await getJSON(API + '/v3/tables/' + tableId + '/bulk-fetch-records', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ recordIds: chunk, includeExternalContentFieldIds: [] }), }); if (res?.results) bulkFetchRecords.push(...res.results); } catch (e) { console.warn('[clay-extract] bulk-fetch batch failed:', e.message); } } if (truncated) { console.warn( '[clay-extract] record pull capped at MAX_RECORDS=' + MAX_RECORDS + ' of ' + recordIds.length + ' total. Raise MAX_RECORDS to pull all.', ); } const extract = { _meta: { extractedAt: new Date().toISOString(), method: 'bookmarklet', tableId, viewId, url: location.href, totalRecordCount, // true table size from /count idCount: recordIds.length, // ids returned fetchedRecordCount: bulkFetchRecords.length, // rows pulled via bulk-fetch (may be sparse/unrun) exampleRecordCount: exampleRecords.length, // rendered sample rows from table-schema-v2 (richest) truncated, // true if MAX_RECORDS capped the pull }, table: table?.table || table, fields: table?.fields || table?.table?.fields || [], // Schema tree keyed by field id (f_xxx → { type, name, children, ... }). tableSchema: schemaTree, // Rendered sample rows: flat { f_xxx: value } maps with formula/action outputs. Prefer these for prompts/samples. exampleRecords, recordIds, // Raw per-cell data for ALL records (cells are sparse — only populated fields appear). bulkFetchRecords, }; return extract; }; const slug = (s, fallback) => String(s || fallback).replace(/[^A-Za-z0-9_-]+/g, '_').slice(0, 60); const download = (payload, filename) => { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 4000); }; try { if (workbookId && !tableId) { // Workbook URL: enumerate child tables and extract each one. toast('fetching workbook tables…'); const wbTables = await getJSON(API + '/v3/workbooks/' + workbookId + '/tables'); const list = Array.isArray(wbTables) ? wbTables : wbTables?.results || wbTables?.tables || []; if (!list.length) throw new Error('workbook returned no tables'); const tables = []; const failures = []; for (let i = 0; i < list.length; i++) { const t = list[i]; const label = '[' + (i + 1) + '/' + list.length + ']'; try { const one = await extractTable(t.id, null, label); one._meta.workbookId = workbookId; one._meta.tableName = t.name || null; tables.push(one); } catch (e) { console.warn('[clay-extract] table ' + t.id + ' failed:', e.message); failures.push({ tableId: t.id, name: t.name || null, error: e.message }); } } const payload = { _meta: { extractedAt: new Date().toISOString(), method: 'bookmarklet', scope: 'workbook', workbookId, url: location.href, tableCount: tables.length, failedCount: failures.length, }, workbook: { id: workbookId, tables: list.map((t) => ({ id: t.id, name: t.name })) }, failures, // One entry per child table, each the same shape a single-table extract produces. tables, }; download(payload, 'clay_extract_workbook_' + slug(workbookId) + '.json'); window.__clayExtract = payload; toast( 'done: ' + tables.length + '/' + list.length + ' tables' + (failures.length ? ' (' + failures.length + ' failed)' : '') + '. Downloaded JSON.', ); } else { const extract = await extractTable(tableId, urlViewId, ''); download(extract, 'clay_extract_' + slug(extract.table?.name, tableId) + '.json'); window.__clayExtract = extract; toast( 'done: ' + (extract.fields?.length || 0) + ' fields, ' + extract.exampleRecords.length + ' sample rows, ' + extract.bulkFetchRecords.length + '/' + (extract._meta.totalRecordCount ?? extract.recordIds.length) + ' records' + (extract._meta.truncated ? ' (CAPPED)' : '') + '. Downloaded JSON.', ); } setTimeout(() => document.getElementById('__clay_extract_toast')?.remove(), 6000); } catch (err) { console.error('[clay-extract]', err); toast('ERROR: ' + err.message + ' (see console)'); } })(); -
clay-extract-bookmarklet.url.txt 8.2 KB
javascript:(async%20()%20=%3E%20{%20const%20API%20=%20'https://api.clay.com';%20const%20MAX_RECORDS%20=%20500;%20const%20BATCH%20=%2050;%20const%20path%20=%20location.pathname;%20const%20tableId%20=%20(path.match(/%5C/tables%5C/(t_[A-Za-z0-9]+)/)%20%7C%7C%20[])[1];%20const%20workbookId%20=%20(path.match(/%5C/workbooks%5C/(wb_[A-Za-z0-9]+)/)%20%7C%7C%20[])[1];%20const%20urlViewId%20=%20(path.match(/%5C/views%5C/(gv_[A-Za-z0-9]+)/)%20%7C%7C%20[])[1];%20if%20(!tableId%20&&%20!workbookId)%20{%20alert(%20'Clay%20extract:%20no%20table%20or%20workbook%20id%20in%20this%20URL.%5Cn%5Cn'%20+%20'Open%20a%20table%20view%20(/tables/t_.../views/gv_...)%20or%20a%20workbook%20'%20+%20'(/workbooks/wb_...)%20and%20run%20this%20again.',%20);%20return;%20}%20const%20toast%20=%20(msg)%20=%3E%20{%20let%20el%20=%20document.getElementById('__clay_extract_toast');%20if%20(!el)%20{%20el%20=%20document.createElement('div');%20el.id%20=%20'__clay_extract_toast';%20el.style.cssText%20=%20'position:fixed;z-index:2147483647;bottom:20px;right:20px;background:%23111;color:%23fff;'%20+%20'font:13px/1.4%20-apple-system,system-ui,sans-serif;padding:10px%2014px;border-radius:8px;'%20+%20'box-shadow:0%204px%2016px%20rgba(0,0,0,.3);max-width:320px';%20document.body.appendChild(el);%20}%20el.textContent%20=%20'Clay%20extract:%20'%20+%20msg;%20return%20el;%20};%20const%20getJSON%20=%20async%20(url,%20opts)%20=%3E%20{%20const%20r%20=%20await%20fetch(url,%20{%20credentials:%20'include',%20headers:%20{%20accept:%20'application/json,%20text/plain,%20*/*',%20...(opts%20&&%20opts.headers)%20},%20...opts,%20});%20if%20(!r.ok)%20throw%20new%20Error(url.replace(API,%20'')%20+%20'%20%E2%86%92%20'%20+%20r.status);%20return%20r.json();%20};%20const%20extractTable%20=%20async%20(tableId,%20preferredViewId,%20label)%20=%3E%20{%20const%20tag%20=%20label%20?%20label%20+%20'%20'%20:%20'';%20toast(tag%20+%20'fetching%20table%20config%E2%80%A6');%20const%20table%20=%20await%20getJSON(API%20+%20'/v3/tables/'%20+%20tableId);%20const%20viewId%20=%20preferredViewId%20%7C%7C%20table?.table?.firstViewId%20%7C%7C%20table?.firstViewId;%20if%20(!viewId)%20throw%20new%20Error('could%20not%20resolve%20a%20view%20id%20(firstViewId%20missing)');%20toast(tag%20+%20'fetching%20schema%20+%20sample%20records%E2%80%A6');%20let%20schemaTree%20=%20null;%20let%20exampleRecords%20=%20[];%20try%20{%20const%20sv2%20=%20await%20getJSON(%20API%20+%20'/v3/tables/'%20+%20tableId%20+%20'/views/'%20+%20viewId%20+%20'/table-schema-v2',%20);%20schemaTree%20=%20sv2?.tableSchema%20%7C%7C%20sv2%20%7C%7C%20null;%20exampleRecords%20=%20sv2?.exampleRecords%20%7C%7C%20sv2?.records%20%7C%7C%20sv2?.sampleRecords%20%7C%7C%20[];%20}%20catch%20(e)%20{%20console.warn('[clay-extract]%20table-schema-v2%20failed:',%20e.message);%20}%20let%20totalRecordCount%20=%20null;%20try%20{%20const%20cnt%20=%20await%20getJSON(API%20+%20'/v3/tables/'%20+%20tableId%20+%20'/count');%20totalRecordCount%20=%20cnt?.tableTotalRecordsCount%20??%20cnt?.count%20??%20null;%20}%20catch%20(e)%20{%20console.warn('[clay-extract]%20count%20failed:',%20e.message);%20}%20toast(tag%20+%20'fetching%20record%20ids%E2%80%A6');%20let%20recordIds%20=%20[];%20try%20{%20const%20ids%20=%20await%20getJSON(%20API%20+%20'/v3/tables/'%20+%20tableId%20+%20'/views/'%20+%20viewId%20+%20'/records/ids',%20);%20recordIds%20=%20ids?.results%20%7C%7C%20ids?.recordIds%20%7C%7C%20ids?.ids%20%7C%7C%20(Array.isArray(ids)%20?%20ids%20:%20[]);%20}%20catch%20(e)%20{%20console.warn('[clay-extract]%20records/ids%20failed:',%20e.message);%20}%20const%20truncated%20=%20recordIds.length%20%3E%20MAX_RECORDS;%20const%20idsToFetch%20=%20recordIds.slice(0,%20MAX_RECORDS);%20const%20bulkFetchRecords%20=%20[];%20for%20(let%20i%20=%200;%20i%20%3C%20idsToFetch.length;%20i%20+=%20BATCH)%20{%20const%20chunk%20=%20idsToFetch.slice(i,%20i%20+%20BATCH);%20toast(tag%20+%20'records%20'%20+%20(i%20+%20chunk.length)%20+%20'/'%20+%20idsToFetch.length%20+%20'%E2%80%A6');%20try%20{%20const%20res%20=%20await%20getJSON(API%20+%20'/v3/tables/'%20+%20tableId%20+%20'/bulk-fetch-records',%20{%20method:%20'POST',%20headers:%20{%20'content-type':%20'application/json'%20},%20body:%20JSON.stringify({%20recordIds:%20chunk,%20includeExternalContentFieldIds:%20[]%20}),%20});%20if%20(res?.results)%20bulkFetchRecords.push(...res.results);%20}%20catch%20(e)%20{%20console.warn('[clay-extract]%20bulk-fetch%20batch%20failed:',%20e.message);%20}%20}%20if%20(truncated)%20{%20console.warn(%20'[clay-extract]%20record%20pull%20capped%20at%20MAX_RECORDS='%20+%20MAX_RECORDS%20+%20'%20of%20'%20+%20recordIds.length%20+%20'%20total.%20Raise%20MAX_RECORDS%20to%20pull%20all.',%20);%20}%20const%20extract%20=%20{%20_meta:%20{%20extractedAt:%20new%20Date().toISOString(),%20method:%20'bookmarklet',%20tableId,%20viewId,%20url:%20location.href,%20totalRecordCount,%20idCount:%20recordIds.length,%20fetchedRecordCount:%20bulkFetchRecords.length,%20exampleRecordCount:%20exampleRecords.length,%20truncated,%20},%20table:%20table?.table%20%7C%7C%20table,%20fields:%20table?.fields%20%7C%7C%20table?.table?.fields%20%7C%7C%20[],%20tableSchema:%20schemaTree,%20exampleRecords,%20recordIds,%20bulkFetchRecords,%20};%20return%20extract;%20};%20const%20slug%20=%20(s,%20fallback)%20=%3E%20String(s%20%7C%7C%20fallback).replace(/[%5EA-Za-z0-9_-]+/g,%20'_').slice(0,%2060);%20const%20download%20=%20(payload,%20filename)%20=%3E%20{%20const%20blob%20=%20new%20Blob([JSON.stringify(payload,%20null,%202)],%20{%20type:%20'application/json'%20});%20const%20a%20=%20document.createElement('a');%20a.href%20=%20URL.createObjectURL(blob);%20a.download%20=%20filename;%20document.body.appendChild(a);%20a.click();%20a.remove();%20setTimeout(()%20=%3E%20URL.revokeObjectURL(a.href),%204000);%20};%20try%20{%20if%20(workbookId%20&&%20!tableId)%20{%20toast('fetching%20workbook%20tables%E2%80%A6');%20const%20wbTables%20=%20await%20getJSON(API%20+%20'/v3/workbooks/'%20+%20workbookId%20+%20'/tables');%20const%20list%20=%20Array.isArray(wbTables)%20?%20wbTables%20:%20wbTables?.results%20%7C%7C%20wbTables?.tables%20%7C%7C%20[];%20if%20(!list.length)%20throw%20new%20Error('workbook%20returned%20no%20tables');%20const%20tables%20=%20[];%20const%20failures%20=%20[];%20for%20(let%20i%20=%200;%20i%20%3C%20list.length;%20i++)%20{%20const%20t%20=%20list[i];%20const%20label%20=%20'['%20+%20(i%20+%201)%20+%20'/'%20+%20list.length%20+%20']';%20try%20{%20const%20one%20=%20await%20extractTable(t.id,%20null,%20label);%20one._meta.workbookId%20=%20workbookId;%20one._meta.tableName%20=%20t.name%20%7C%7C%20null;%20tables.push(one);%20}%20catch%20(e)%20{%20console.warn('[clay-extract]%20table%20'%20+%20t.id%20+%20'%20failed:',%20e.message);%20failures.push({%20tableId:%20t.id,%20name:%20t.name%20%7C%7C%20null,%20error:%20e.message%20});%20}%20}%20const%20payload%20=%20{%20_meta:%20{%20extractedAt:%20new%20Date().toISOString(),%20method:%20'bookmarklet',%20scope:%20'workbook',%20workbookId,%20url:%20location.href,%20tableCount:%20tables.length,%20failedCount:%20failures.length,%20},%20workbook:%20{%20id:%20workbookId,%20tables:%20list.map((t)%20=%3E%20({%20id:%20t.id,%20name:%20t.name%20}))%20},%20failures,%20tables,%20};%20download(payload,%20'clay_extract_workbook_'%20+%20slug(workbookId)%20+%20'.json');%20window.__clayExtract%20=%20payload;%20toast(%20'done:%20'%20+%20tables.length%20+%20'/'%20+%20list.length%20+%20'%20tables'%20+%20(failures.length%20?%20'%20('%20+%20failures.length%20+%20'%20failed)'%20:%20'')%20+%20'.%20Downloaded%20JSON.',%20);%20}%20else%20{%20const%20extract%20=%20await%20extractTable(tableId,%20urlViewId,%20'');%20download(extract,%20'clay_extract_'%20+%20slug(extract.table?.name,%20tableId)%20+%20'.json');%20window.__clayExtract%20=%20extract;%20toast(%20'done:%20'%20+%20(extract.fields?.length%20%7C%7C%200)%20+%20'%20fields,%20'%20+%20extract.exampleRecords.length%20+%20'%20sample%20rows,%20'%20+%20extract.bulkFetchRecords.length%20+%20'/'%20+%20(extract._meta.totalRecordCount%20??%20extract.recordIds.length)%20+%20'%20records'%20+%20(extract._meta.truncated%20?%20'%20(CAPPED)'%20:%20'')%20+%20'.%20Downloaded%20JSON.',%20);%20}%20setTimeout(()%20=%3E%20document.getElementById('__clay_extract_toast')?.remove(),%206000);%20}%20catch%20(err)%20{%20console.error('[clay-extract]',%20err);%20toast('ERROR:%20'%20+%20err.message%20+%20'%20(see%20console)');%20}%20})(); -
clay-extract.py 17.9 KB
#!/usr/bin/env python3 """ Clay Table Config Extractor Extracts Clay table configs (fields, prompts, action settings, sample records) via Clay's internal API. Usage: # First time: paste a cURL from Chrome DevTools to save your session python3 scripts/clay-extract.py --auth # (In Chrome: DevTools → Network → any api.clay.com request → Copy as cURL → paste) # Extract a specific table python3 scripts/clay-extract.py https://app.clay.com/workspaces/502058/workbooks/wb_xxx/tables/t_xxx # List all tables in a workspace folder python3 scripts/clay-extract.py https://app.clay.com/workspaces/502058/home/f_xxx # Extract by table ID directly python3 scripts/clay-extract.py t_0t5pj9mqNnpxxjM6jaV Session is saved to .clay-session.json and reused until it expires. Output goes to tmp/clay_extract_<table_name>.json (won't overwrite existing files). """ import json import os import re import sys import time from pathlib import Path from urllib.parse import urlparse SESSION_FILE = Path(".clay-session.json") # --------------------------------------------------------------------------- # Session management # --------------------------------------------------------------------------- def extract_cookie_from_curl(curl_str: str) -> str | None: """Parse a 'Copy as cURL' string to extract the Cookie header.""" # Match -b 'cookie...' or --cookie 'cookie...' or -H 'cookie: ...' patterns = [ r"-b\s+'([^']+)'", r"-b\s+\"([^\"]+)\"", r"--cookie\s+'([^']+)'", r"--cookie\s+\"([^\"]+)\"", r"-H\s+'[Cc]ookie:\s*([^']+)'", r'-H\s+"[Cc]ookie:\s*([^"]+)"', ] for pattern in patterns: match = re.search(pattern, curl_str) if match: cookie = match.group(1).strip() if "claysession" in cookie: return cookie return None def save_session(cookie: str): """Persist cookie for reuse.""" SESSION_FILE.write_text(json.dumps({ "cookie": cookie, "savedAt": time.time(), }, indent=2)) gitignore = Path(".gitignore") if gitignore.exists(): content = gitignore.read_text() if ".clay-session.json" not in content: with open(gitignore, "a") as f: f.write("\n.clay-session.json\n") print(f"[OK] Session saved to {SESSION_FILE}") def get_saved_session() -> str | None: """Load saved cookie if still fresh.""" if not SESSION_FILE.exists(): return None try: data = json.loads(SESSION_FILE.read_text()) cookie = data.get("cookie", "") saved_at = data.get("savedAt", 0) if time.time() - saved_at > 20 * 3600: return None if "claysession" not in cookie: return None return cookie except Exception: return None def test_session(cookie: str) -> bool: """Quick check if the cookie is still valid.""" import requests try: # /v3/users/me returns 403; use workspace resources as a lightweight auth check resp = requests.get( "https://api.clay.com/v3/actions?workspaceId=502058", headers={"accept": "application/json", "cookie": cookie, "origin": "https://app.clay.com"}, timeout=10, ) return resp.status_code == 200 except Exception: return False def get_clay_cookie_from_env() -> str | None: """Read CLAY_COOKIE from .env.deepline if it exists.""" env_file = Path(".env.deepline") if not env_file.exists(): return None for line in env_file.read_text().splitlines(): line = line.strip() if line.startswith("CLAY_COOKIE="): val = line[len("CLAY_COOKIE="):] if (val.startswith("'") and val.endswith("'")) or (val.startswith('"') and val.endswith('"')): val = val[1:-1] return val return None def do_auth(): """Interactive auth: user pastes a cURL command.""" print("Paste a cURL command from Chrome DevTools (any api.clay.com request):") print(" Chrome → DevTools (Cmd+Option+I) → Network → click any api.clay.com request") print(" → Right-click → Copy → Copy as cURL") print() lines = [] print("Paste here (press Enter twice when done):") empty_count = 0 while True: try: line = input() if not line.strip(): empty_count += 1 if empty_count >= 1 and lines: break else: empty_count = 0 lines.append(line) except EOFError: break curl_str = " ".join(lines) cookie = extract_cookie_from_curl(curl_str) if not cookie: print("[FAIL] Could not find claysession in the pasted cURL.") print(" Make sure you're copying from an api.clay.com request while logged in.") sys.exit(1) if test_session(cookie): save_session(cookie) print("[OK] Session is valid. You can now run extraction commands.") else: print("[FAIL] Cookie found but session is invalid/expired. Try copying a fresh cURL.") sys.exit(1) def get_cookie() -> str: """Get a valid Clay session cookie.""" # 1. Saved session cookie = get_saved_session() if cookie and test_session(cookie): print("[OK] Using saved Clay session") return cookie # 2. .env.deepline cookie = get_clay_cookie_from_env() if cookie and test_session(cookie): print("[OK] Using Clay session from .env.deepline") save_session(cookie) return cookie # 3. Need auth print("[AUTH] No valid session found. Run with --auth first:") print(f" python3 {sys.argv[0]} --auth") sys.exit(1) # --------------------------------------------------------------------------- # Clay API client # --------------------------------------------------------------------------- class ClayAPI: BASE = "https://api.clay.com" def __init__(self, cookie: str): import requests self.session = requests.Session() self.session.headers.update({ "accept": "application/json", "content-type": "application/json", "cookie": cookie, "origin": "https://app.clay.com", "referer": "https://app.clay.com/", }) def get(self, path: str): resp = self.session.get(f"{self.BASE}{path}") if resp.status_code == 401: print(f"[FAIL] 401 on {path} — session expired. Run: python3 {sys.argv[0]} --auth") sys.exit(1) resp.raise_for_status() return resp.json() def post(self, path: str, body: dict = None): resp = self.session.post(f"{self.BASE}{path}", json=body or {}) if resp.status_code == 401: print(f"[FAIL] 401 on {path} — session expired. Run: python3 {sys.argv[0]} --auth") sys.exit(1) resp.raise_for_status() return resp.json() def get_table_config(self, table_id: str) -> dict: print(f" Fetching table config for {table_id}...") return self.get(f"/v3/tables/{table_id}") def get_table_schema_v2(self, table_id: str, view_id: str) -> dict: print(f" Fetching schema + example records...") return self.get(f"/v3/tables/{table_id}/views/{view_id}/table-schema-v2") def list_workspace_resources(self, workspace_id: str) -> dict: print(f" Listing workspace {workspace_id} resources...") return self.post(f"/v3/workspaces/{workspace_id}/resources_v2/") def list_workbook_tables(self, workbook_id: str) -> list[dict]: """Get tables from a workbook (returns list of {id, name}).""" print(f" Listing tables in workbook {workbook_id}...") tables = self.get(f"/v3/workbooks/{workbook_id}/tables") if isinstance(tables, list): return [{"id": t["id"], "name": t.get("name", "")} for t in tables if "id" in t] return [] def search_workbooks(self, workspace_id: str, query: str) -> list[dict]: """Search workspace workbooks by name (case-insensitive substring match).""" resources = self.list_workspace_resources(workspace_id) workbooks = [ r for r in resources.get("resources", []) if r.get("resourceType") == "WORKBOOK" ] q = query.lower() matches = [wb for wb in workbooks if q in wb.get("name", "").lower()] return matches def extract_table(self, table_id: str) -> dict: config = self.get_table_config(table_id) table = config.get("table", config) view_id = table.get("firstViewId") result = { "_meta": { "extractedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "method": "clay-extract.py", "tableId": table_id, }, "table": { "id": table.get("id"), "name": table.get("name"), "workbookId": table.get("workbookId"), "workspaceId": table.get("workspaceId"), "firstViewId": view_id, "tableSettings": table.get("tableSettings"), }, "fields": [], "exampleRecords": [], } for f in table.get("fields", []): result["fields"].append({ "id": f.get("id"), "name": f.get("name"), "type": f.get("type"), "actionType": f.get("actionType"), "inputFieldIds": f.get("inputFieldIds", []), "typeSettings": f.get("typeSettings"), }) if view_id: try: schema_data = self.get_table_schema_v2(table_id, view_id) result["tableSchema"] = schema_data.get("tableSchema") result["exampleRecords"] = schema_data.get("exampleRecords", []) except Exception as e: print(f" [WARN] Could not fetch schema-v2: {e}") return result # --------------------------------------------------------------------------- # URL parsing # --------------------------------------------------------------------------- def parse_clay_input(arg: str, workspace_id: str | None = None): # Direct table ID if re.match(r"^t_[a-zA-Z0-9]+$", arg): return {"type": "table", "table_id": arg} # Direct workbook ID if re.match(r"^wb_[a-zA-Z0-9]+$", arg): return {"type": "workbook", "workbook_id": arg, "workspace_id": workspace_id} # URL parsing parsed = urlparse(arg) path = parsed.path if path.startswith("/"): table_match = re.search(r"/tables/(t_[a-zA-Z0-9]+)", path) if table_match: return {"type": "table", "table_id": table_match.group(1)} wb_match = re.search(r"/workspaces/(\d+)/workbooks/(wb_[a-zA-Z0-9]+)", path) if wb_match: return {"type": "workbook", "workspace_id": wb_match.group(1), "workbook_id": wb_match.group(2)} folder_match = re.search(r"/workspaces/(\d+)/home/(f_[a-zA-Z0-9]+)", path) if folder_match: return {"type": "folder", "workspace_id": folder_match.group(1), "folder_id": folder_match.group(2)} ws_match = re.search(r"/workspaces/(\d+)", path) if ws_match: return {"type": "workspace", "workspace_id": ws_match.group(1)} # Name-based search — anything that's not an ID or URL return {"type": "search", "query": arg, "workspace_id": workspace_id} # --------------------------------------------------------------------------- # Output # --------------------------------------------------------------------------- def save_extract(extract: dict, output_dir: Path): table_name = extract["table"]["name"] or extract["table"]["id"] safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", table_name).strip("_").lower() filename = f"clay_extract_{safe_name}.json" output_path = output_dir / filename if output_path.exists(): i = 2 while True: alt = output_dir / f"clay_extract_{safe_name}_{i}.json" if not alt.exists(): output_path = alt break i += 1 output_path.write_text(json.dumps(extract, indent=2, default=str)) return output_path def print_summary(extract: dict): table = extract["table"] fields = extract["fields"] records = extract.get("exampleRecords", []) print(f"\n{'='*60}") print(f"Table: {table['name']} ({table['id']})") print(f"Fields: {len(fields)} | Example records: {len(records)}") print(f"{'='*60}") for f in fields: ts = f.get("typeSettings") or {} action_key = ts.get("actionKey", "") ftype = f["type"] marker = "" if ftype == "action": marker = f" [{action_key}]" elif ftype == "source": marker = " [source]" print(f" {f['id'][:20]:20s} {ftype:8s} {f['name'][:40]}{marker}") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): if len(sys.argv) < 2: print(__doc__) sys.exit(1) if sys.argv[1] == "--auth": do_auth() return # Parse args: [--workspace WORKSPACE_ID] <query> args = sys.argv[1:] workspace_id = None if "--workspace" in args: idx = args.index("--workspace") workspace_id = args[idx + 1] args = args[:idx] + args[idx + 2:] arg = " ".join(args) output_dir = Path("tmp") output_dir.mkdir(exist_ok=True) info = parse_clay_input(arg, workspace_id) print(f"[INPUT] Parsed as: {info['type']}") cookie = get_cookie() api = ClayAPI(cookie) extracts = [] if info["type"] == "table": extracts.append(api.extract_table(info["table_id"])) elif info["type"] == "workbook": tables = api.list_workbook_tables(info["workbook_id"]) if not tables: print(" [FAIL] No tables found in workbook.") sys.exit(1) print(f" Found {len(tables)} table(s)") for t in tables: print(f"\n--- Extracting: {t['name'] or t['id']} ---") try: extracts.append(api.extract_table(t["id"])) except Exception as e: print(f" [ERROR] {t['id']}: {e}") elif info["type"] == "search": ws_id = info.get("workspace_id") if not ws_id: # Try to get workspace ID from saved session or recent extracts print("[FAIL] Name search requires a workspace ID.") print(" Use: python3 scripts/clay-extract.py --workspace 502058 \"Demo Requests\"") print(" Or provide a full Clay URL instead.") sys.exit(1) query = info["query"] print(f" Searching workspace {ws_id} for \"{query}\"...") matches = api.search_workbooks(ws_id, query) if not matches: print(f" No workbooks matching \"{query}\"") # Also search table names inside all workbooks print(" Searching table names...") resources = api.list_workspace_resources(ws_id) all_wbs = [r for r in resources.get("resources", []) if r.get("resourceType") == "WORKBOOK"] for wb in all_wbs: try: tables = api.list_workbook_tables(wb["id"]) for t in tables: if query.lower() in t.get("name", "").lower(): print(f" Found table: {t['name']} in workbook \"{wb['name']}\"") extracts.append(api.extract_table(t["id"])) except Exception: pass if not extracts: print(f" [FAIL] No workbooks or tables matching \"{query}\"") sys.exit(1) else: print(f" Found {len(matches)} workbook(s):") for m in matches: print(f" {m['id']} — {m['name']}") for wb in matches: tables = api.list_workbook_tables(wb["id"]) for t in tables: print(f"\n--- Extracting: {t['name'] or t['id']} (from \"{wb['name']}\") ---") try: extracts.append(api.extract_table(t["id"])) except Exception as e: print(f" [ERROR] {t['id']}: {e}") elif info["type"] in ("workspace", "folder"): try: resources = api.list_workspace_resources(info["workspace_id"]) workbook_ids = [ r["id"] for r in resources.get("resources", []) if r.get("resourceType") == "WORKBOOK" ] except Exception as e: workbook_ids = [] print(f" [WARN] resources_v2 failed: {e}") if info["type"] == "folder": print(f" [NOTE] Folder children aren't in the API response.") print(f" Found {len(workbook_ids)} top-level workbooks.") if not workbook_ids: sys.exit(1) print(f" Resolving {len(workbook_ids)} workbooks to tables...") table_ids = [] for wb_id in workbook_ids: try: tables = api.list_workbook_tables(wb_id) table_ids.extend([t["id"] for t in tables]) except Exception: pass if not table_ids: print(" [FAIL] No tables found.") sys.exit(1) print(f" Found {len(table_ids)} table(s) total") for tid in table_ids: print(f"\n--- Extracting: {tid} ---") try: extracts.append(api.extract_table(tid)) except Exception as e: print(f" [ERROR] {tid}: {e}") if not extracts: print("\n[FAIL] No tables extracted.") sys.exit(1) for extract in extracts: print_summary(extract) path = save_extract(extract, output_dir) print(f" Saved to: {path}") print(f"\n[DONE] Extracted {len(extracts)} table(s)") if __name__ == "__main__": main() -
clay-har-miner.py 5.8 KB
#!/usr/bin/env python3 """Mine Clay HAR captures into a full endpoint surface map. Normalizes ids (t_xxx, gv_xxx, wb_xxx, f_xxx, r_xxx, numeric) into {placeholders} so distinct routes collapse into one row each. """ import json, re, sys, glob, os from collections import defaultdict # Each pattern is anchored to a COMPLETE path segment via (?=/|$). Without that # boundary '/v3/12345abc' would normalize to '/v3/{NUM_ID}abc' and collapse # unrelated routes. UUIDs are matched case-insensitively so uppercase variants # do not fragment an otherwise identical route. _END = r'(?=/|$)' ID_PATTERNS = [ (re.compile(r'/t_[A-Za-z0-9]{8,}' + _END), '/{TABLE_ID}'), (re.compile(r'/gv_[A-Za-z0-9]{8,}' + _END), '/{VIEW_ID}'), (re.compile(r'/wb_[A-Za-z0-9]{8,}' + _END), '/{WORKBOOK_ID}'), (re.compile(r'/f_[A-Za-z0-9]{8,}' + _END), '/{FIELD_ID}'), (re.compile(r'/r_[A-Za-z0-9]{8,}' + _END), '/{RECORD_ID}'), (re.compile(r'/ws_[A-Za-z0-9]{8,}' + _END), '/{WORKSPACE_ID}'), (re.compile(r'/src_[A-Za-z0-9]{8,}' + _END), '/{SOURCE_ID}'), (re.compile(r'/s_[A-Za-z0-9]{8,}' + _END), '/{SOURCE_ID}'), (re.compile(r'/aa_[A-Za-z0-9]{8,}' + _END), '/{APP_ACCOUNT_ID}'), (re.compile(r'/act_[A-Za-z0-9]{8,}' + _END), '/{ACTION_ID}'), (re.compile(r'/fol_[A-Za-z0-9]{8,}' + _END), '/{FOLDER_ID}'), (re.compile( r'/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' + _END, re.IGNORECASE), '/{UUID}'), (re.compile(r'/\d{4,}' + _END), '/{NUM_ID}'), ] def norm(path): for pat, rep in ID_PATTERNS: path = pat.sub(rep, path) return path def shape(v, depth=0): """Compact type-shape of a JSON value.""" if depth > 2: return '...' if isinstance(v, dict): if not v: return '{}' ks = list(v.keys())[:12] return '{' + ','.join(ks) + (',...' if len(v) > 12 else '') + '}' if isinstance(v, list): if not v: return '[]' return '[' + shape(v[0], depth + 1) + ']' return type(v).__name__ def main(files): routes = defaultdict(lambda: { 'methods': set(), 'statuses': set(), 'count': 0, 'req_shapes': set(), 'resp_shapes': set(), 'queries': set(), 'sources': set(), 'example': None, }) for fp in files: name = os.path.basename(fp) try: har = json.load(open(fp)) except Exception as e: print(f' !! {name}: {e}', file=sys.stderr) continue log = har.get('log') if isinstance(har, dict) else None entries = log.get('entries') if isinstance(log, dict) else None if not isinstance(entries, list): print(f' !! {name}: no log.entries array', file=sys.stderr) continue for e in entries: if not isinstance(e, dict): continue req = e.get('request') req = req if isinstance(req, dict) else {} url = req.get('url') or '' if not isinstance(url, str) or 'clay.com' not in url: continue m = re.match(r'https?://([^/]+)(/[^?#]*)(\?[^#]*)?', url) if not m: continue host, path, qs = m.group(1), m.group(2), (m.group(3) or '') # only API hosts, skip static assets if not (host.startswith('api.') or '/v3/' in path or '/v1/' in path or '/api/' in path): continue if re.search(r'\.(js|css|png|jpg|svg|woff2?|ico|map)$', path): continue key = (host, norm(path)) r = routes[key] resp = e.get('response') resp = resp if isinstance(resp, dict) else {} r['methods'].add(req.get('method') or '?') r['statuses'].add(resp.get('status') or 0) r['count'] += 1 r['sources'].add(name) if qs: for part in qs.lstrip('?').split('&'): if '=' in part: r['queries'].add(part.split('=')[0]) post = req.get('postData') pd = post.get('text') if isinstance(post, dict) else None if isinstance(pd, str) and pd: try: r['req_shapes'].add(shape(json.loads(pd))) except Exception: pass content = resp.get('content') content = content if isinstance(content, dict) else {} txt = content.get('text') mime = content.get('mimeType') or '' if isinstance(txt, str) and txt and isinstance(mime, str) and mime.startswith('application/json'): try: r['resp_shapes'].add(shape(json.loads(txt))) except Exception: pass if r['example'] is None: r['example'] = path return routes if __name__ == '__main__': files = sys.argv[1:] or glob.glob(os.path.expanduser('~/Downloads/app.clay.com*.har')) print(f'mining {len(files)} HAR files...', file=sys.stderr) routes = main(files) out = [] for (host, path), r in sorted(routes.items(), key=lambda kv: (-kv[1]['count'], kv[0])): out.append({ 'host': host, 'path': path, 'methods': sorted(r['methods']), 'statuses': sorted(r['statuses']), 'calls': r['count'], 'queryParams': sorted(r['queries'])[:15], 'requestShapes': sorted(r['req_shapes'])[:3], 'responseShapes': sorted(r['resp_shapes'])[:3], 'seenIn': sorted(r['sources']), 'example': r['example'], }) outdir = os.path.dirname(os.path.abspath(sys.argv[0])) json.dump(out, open(os.path.join(outdir, 'clay_endpoints.json'), 'w'), indent=2) print(f'{len(out)} distinct routes -> clay_endpoints.json', file=sys.stderr) for o in out: print(f"{','.join(o['methods']):12} {o['host']}{o['path']} [{o['calls']}x, {o['statuses']}]") -
contact-accuracy-audit.py 8.9 KB
#!/usr/bin/env python3 """ Audit final B2B contact rows before shipping. The script is deterministic by design: it checks freshness, email risk, identity confidence, final-cell shape, domain alignment, and duplicate-person conflicts, then projects those flags into ACTION and flag_reason columns. Usage: python3 contact-accuracy-audit.py final.csv > audited.csv python3 contact-accuracy-audit.py --fixtures fixtures_contact_accuracy_audit.json """ import argparse import csv import json import re import sys from datetime import date, datetime EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") def norm(s): return re.sub(r"[^a-z0-9]", "", (s or "").lower()) def norm_domain(s): s = (s or "").strip().lower() s = re.sub(r"^https?://", "", s) s = re.sub(r"^www\.", "", s) return s.split("/")[0] def norm_linkedin_url(s): s = (s or "").strip().lower() m = re.search(r"linkedin\.com/in/([^/?#]+)", s) return m.group(1).strip("/") if m else "" def split_domains(s): return [norm_domain(p) for p in re.split(r"[,; ]+", s or "") if norm_domain(p)] def parse_date(s): s = (s or "").strip() if not s: return None for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y-%m", "%Y"): try: parsed = datetime.strptime(s, fmt) return parsed.date() except ValueError: continue return None def days_old(s, today): parsed = parse_date(s) if not parsed: return None return (today - parsed).days def email_domain(email): if "@" not in (email or ""): return "" return norm_domain(email.rsplit("@", 1)[1]) def person_key(row): linkedin = norm_linkedin_url(row.get("linkedin_url") or row.get("linkedin")) if linkedin: return f"li:{linkedin}" email = (row.get("email") or "").strip().lower() if EMAIL_RE.match(email): return f"email:{email}" name = norm(row.get("name") or f"{row.get('first_name', '')} {row.get('last_name', '')}") domain = norm_domain(row.get("company_domain") or row.get("domain")) title = norm(row.get("current_title") or row.get("title")) return f"name:{name}:{domain}:{title}" if name and domain else "" def flag(row, today): flags = [] email = (row.get("email") or "").strip() company = row.get("company") or row.get("company_name") or "" current_company = row.get("current_company") or row.get("company") or "" title = row.get("current_title") or row.get("title") or "" company_domain = norm_domain(row.get("company_domain") or row.get("domain")) allowed_domains = set(split_domains(row.get("allowed_email_domains"))) if company_domain: allowed_domains.add(company_domain) changed_company = current_company and company and norm(current_company) != norm(company) if changed_company: flags.append("job_changed") if email: if not EMAIL_RE.match(email): flags.append("invalid_email_format") elif changed_company and company_domain and email_domain(email) == company_domain: flags.append("email_domain_mismatch") elif allowed_domains and email_domain(email) not in allowed_domains: flags.append("email_domain_mismatch") else: flags.append("missing_email") linkedin = row.get("linkedin_url") or row.get("linkedin") or "" if linkedin and not norm_linkedin_url(linkedin): flags.append("invalid_linkedin_url") if title and (norm(title) == norm(company) or norm(title) == norm(current_company)): flags.append("title_equals_company") profile_age = days_old(row.get("profile_scraped_at") or row.get("source_observed_at"), today) email_age = days_old(row.get("email_verified_at"), today) if profile_age is None: flags.append("profile_freshness_missing") elif profile_age > 30: flags.append("profile_stale") if email_age is None: flags.append("email_verification_missing") elif email_age > 30: flags.append("email_verification_stale") identity = (row.get("identity_confirmation") or "").strip().upper() if not identity or identity == "NONE": flags.append("identity_unconfirmed") validation = (row.get("email_validation") or row.get("validation_status") or "").strip().lower() corroboration = int(row.get("catch_all_corroboration_count") or 0) email_risk = "LOW" if "catch-all" in validation or "catch_all" in validation: if corroboration >= 2: email_risk = "MED" else: email_risk = "HIGH" flags.append("catch_all_not_corroborated") elif "invalid" in validation or "do_not_mail" in validation: email_risk = "HIGH" flags.append("email_invalid") elif "unknown" in validation or "no-status" in validation or not validation: email_risk = "MED" flags.append("email_validation_unknown") return flags, email_risk, profile_age, email_age def project_action(flags, confidence, email_risk): confidence = (confidence or "").strip().upper() if "job_changed" in flags: return "REMOVE / RE-TARGET" if "duplicate_person_conflict" in flags or "catch_all_not_corroborated" in flags: return "REVIEW" verify_flags = { "invalid_email_format", "invalid_linkedin_url", "title_equals_company", "profile_stale", "profile_freshness_missing", "email_verification_stale", "email_verification_missing", "identity_unconfirmed", "email_invalid", "email_validation_unknown", "missing_email", } if any(f in verify_flags for f in flags): return "VERIFY" if confidence in ("LOW", "HOLD"): return "VERIFY" if email_risk == "HIGH": return "REVIEW" return "SEND" def audit_rows(rows, today): audited = [] for row in rows: flags, email_risk, profile_age, email_age = flag(row, today) out = dict(row) out["_flags"] = flags out["email_risk"] = email_risk out["profile_age_days"] = "" if profile_age is None else str(profile_age) out["email_verification_age_days"] = "" if email_age is None else str(email_age) audited.append(out) groups = {} for idx, row in enumerate(audited): key = person_key(row) if key: groups.setdefault(key, []).append(idx) for indexes in groups.values(): if len(indexes) < 2: continue emails = {(audited[i].get("email") or "").strip().lower() for i in indexes} titles = {norm(audited[i].get("current_title") or audited[i].get("title")) for i in indexes} companies = {norm(audited[i].get("current_company") or audited[i].get("company")) for i in indexes} if len(emails) > 1 or len(titles) > 1 or len(companies) > 1: for i in indexes: audited[i]["_flags"].append("duplicate_person_conflict") for row in audited: flags = sorted(set(row.pop("_flags"))) row["flags"] = ",".join(flags) row["flag_reason"] = "; ".join(flags) row["ACTION"] = project_action(flags, row.get("gold_confidence") or row.get("GOLD_confidence"), row["email_risk"]) return audited def run_fixtures(path, today): cases = json.load(open(path)) passed = failed = 0 for case in cases: got = audit_rows(case["rows"], today) expected = case["expected"] ok = len(got) == len(expected) if ok: for out, exp in zip(got, expected): if exp.get("action") and out["ACTION"] != exp["action"]: ok = False if exp.get("email_risk") and out["email_risk"] != exp["email_risk"]: ok = False for expected_flag in exp.get("flags", []): if expected_flag not in out["flags"].split(","): ok = False if ok: passed += 1 else: failed += 1 print(f" FAIL [{case.get('name', '?')}] expected {expected} got {got}") print(f"\n{passed} passed, {failed} failed") sys.exit(1 if failed else 0) def main(): ap = argparse.ArgumentParser() ap.add_argument("csv", nargs="?") ap.add_argument("--fixtures") ap.add_argument("--today", default="2026-06-07") args = ap.parse_args() today = parse_date(args.today) or date.today() if args.fixtures: run_fixtures(args.fixtures, today) return if not args.csv: print("ERROR: provide a CSV path or --fixtures", file=sys.stderr) sys.exit(2) rows = list(csv.DictReader(open(args.csv))) audited = audit_rows(rows, today) fields = list(rows[0].keys()) if rows else [] for col in ("email_risk", "profile_age_days", "email_verification_age_days", "flags", "flag_reason", "ACTION"): if col not in fields: fields.append(col) writer = csv.DictWriter(sys.stdout, fieldnames=fields) writer.writeheader() writer.writerows(audited) if __name__ == "__main__": main() -
fixtures_contact_accuracy_audit.json 5.5 KB
[ { "name": "send-valid-current-work-email", "rows": [ { "name": "Avery Chen", "company": "Acme Cloud", "company_domain": "acmecloud.com", "current_company": "Acme Cloud", "current_title": "VP Engineering", "linkedin_url": "https://www.linkedin.com/in/averychen", "email": "avery.chen@acmecloud.com", "email_validation": "zerobounce:valid", "profile_scraped_at": "2026-06-01", "email_verified_at": "2026-06-06", "identity_confirmation": "linkedin_experience", "gold_confidence": "HIGH" } ], "expected": [ {"action": "SEND", "email_risk": "LOW", "flags": []} ] }, { "name": "catch-all-needs-corroboration", "rows": [ { "name": "Jordan Lee", "company": "Globex", "company_domain": "globex.com", "current_company": "Globex", "current_title": "Head of Sales", "linkedin_url": "https://linkedin.com/in/jordanlee", "email": "jordan.lee@globex.com", "email_validation": "catch-all", "catch_all_corroboration_count": "1", "profile_scraped_at": "2026-06-01", "email_verified_at": "2026-06-05", "identity_confirmation": "crustdata_domain", "gold_confidence": "MEDIUM" } ], "expected": [ {"action": "REVIEW", "email_risk": "HIGH", "flags": ["catch_all_not_corroborated"]} ] }, { "name": "job-changer-removes-or-retargets", "rows": [ { "name": "Morgan Patel", "company": "OldCo", "company_domain": "oldco.com", "current_company": "NewCo", "current_title": "CFO", "linkedin_url": "https://linkedin.com/in/morganpatel", "email": "morgan@oldco.com", "email_validation": "zerobounce:valid", "profile_scraped_at": "2026-06-03", "email_verified_at": "2026-06-04", "identity_confirmation": "linkedin_experience", "gold_confidence": "HIGH" } ], "expected": [ {"action": "REMOVE / RE-TARGET", "flags": ["job_changed", "email_domain_mismatch"]} ] }, { "name": "stale-verification-holds", "rows": [ { "name": "Casey Stone", "company": "FreshCo", "company_domain": "freshco.com", "current_company": "FreshCo", "current_title": "Director of Operations", "linkedin_url": "https://linkedin.com/in/caseystone", "email": "casey@freshco.com", "email_validation": "zerobounce:valid", "profile_scraped_at": "2026-03-01", "email_verified_at": "2026-04-15", "identity_confirmation": "linkedin_experience", "gold_confidence": "HIGH" } ], "expected": [ {"action": "VERIFY", "flags": ["profile_stale", "email_verification_stale"]} ] }, { "name": "allowed-company-email-alias", "rows": [ { "name": "Riley Gomez", "company": "Acme Cloud", "company_domain": "acmecloud.com", "allowed_email_domains": "acme.io, acmecloud.com", "current_company": "Acme Cloud", "current_title": "Chief Revenue Officer", "linkedin_url": "https://linkedin.com/in/rileygomez", "email": "riley@acme.io", "email_validation": "zerobounce:valid", "profile_scraped_at": "2026-06-02", "email_verified_at": "2026-06-06", "identity_confirmation": "email_domain_match", "gold_confidence": "HIGH" } ], "expected": [ {"action": "SEND", "flags": []} ] }, { "name": "malformed-cell-checks", "rows": [ { "name": "Taylor Brooks", "company": "DataWorks", "company_domain": "dataworks.com", "current_company": "DataWorks", "current_title": "DataWorks", "linkedin_url": "https://linkedin.com/search/results/people/?keywords=Taylor", "email": "not-an-email", "email_validation": "zerobounce:valid", "profile_scraped_at": "2026-06-02", "email_verified_at": "2026-06-06", "identity_confirmation": "linkedin_experience", "gold_confidence": "HIGH" } ], "expected": [ {"action": "VERIFY", "flags": ["invalid_email_format", "invalid_linkedin_url", "title_equals_company"]} ] }, { "name": "duplicate-person-conflict", "rows": [ { "name": "Jamie Park", "company": "Northwind", "company_domain": "northwind.com", "current_company": "Northwind", "current_title": "Controller", "linkedin_url": "https://linkedin.com/in/jamiepark", "email": "jamie@northwind.com", "email_validation": "zerobounce:valid", "profile_scraped_at": "2026-06-02", "email_verified_at": "2026-06-06", "identity_confirmation": "linkedin_experience", "gold_confidence": "HIGH" }, { "name": "Jamie Park", "company": "Northwind", "company_domain": "northwind.com", "current_company": "Northwind", "current_title": "Controller", "linkedin_url": "https://www.linkedin.com/in/jamiepark/", "email": "jamie.park@northwind.com", "email_validation": "zerobounce:valid", "profile_scraped_at": "2026-06-02", "email_verified_at": "2026-06-06", "identity_confirmation": "linkedin_experience", "gold_confidence": "HIGH" } ], "expected": [ {"action": "REVIEW", "flags": ["duplicate_person_conflict"]}, {"action": "REVIEW", "flags": ["duplicate_person_conflict"]} ] } ] -
fixtures_current_role.json 10.3 KB
[ { "name": "stale-toplevel-jobtitle-military", "profile": { "jobTitle": "Platoon Sergeant", "companyName": "United States Marine Corps", "headline": "Corporate Controller", "experiences": [ {"title": "Platoon Sergeant", "companyName": "United States Marine Corps", "jobStartedOn": "1995", "jobEndedOn": "2001", "jobStillWorking": false}, {"title": "Corporate Controller", "companyName": "CEIS", "jobStartedOn": "07-2025", "jobStillWorking": true} ] }, "expected": {"title": "Corporate Controller", "company": "CEIS", "role_kind": "work_current"} }, { "name": "stale-toplevel-deloitte", "profile": { "jobTitle": "Senior Associate", "companyName": "Deloitte", "experiences": [ {"title": "Senior Associate", "companyName": "Deloitte", "jobStartedOn": "2000", "jobStillWorking": false}, {"title": "Chief Financial Officer", "companyName": "Accent", "jobStartedOn": "03-2026", "jobStillWorking": true} ] }, "expected": {"title": "Chief Financial Officer", "company": "Accent", "role_kind": "work_current"} }, { "name": "board-seat-vs-real-job", "profile": { "jobTitle": "Board Member", "companyName": "DXP", "experiences": [ {"title": "Board Member", "companyName": "DXP", "jobStartedOn": "2021", "jobStillWorking": true}, {"title": "CFO", "companyName": "DXP Enterprises", "jobStartedOn": "2018", "jobStillWorking": true} ] }, "expected": {"title": "CFO", "company": "DXP Enterprises", "role_kind": "work_current"} }, { "name": "charity-board-only-hold", "profile": { "jobTitle": "President of the Board of Directors", "companyName": "Trees For Houston", "experiences": [ {"title": "President of the Board of Directors", "companyName": "Trees For Houston", "jobStartedOn": "2020", "jobStillWorking": true} ] }, "expected": {"title": "President of the Board of Directors", "company": "Trees For Houston", "role_kind": "nonwork_only_current"} }, { "name": "company-name-in-title-repaired-via-headline", "profile": { "jobTitle": "Board Member", "companyName": "DXP Enterprises, Inc.", "headline": "SVP & Chief Financial Officer (CFO), Board Member\nDXP Enterprises, Inc.", "experiences": [ {"title": "DXP Enterprises, Inc.", "companyName": "DXP Enterprises, Inc.", "jobStartedOn": "2021-04", "jobStillWorking": true} ] }, "expected": {"title": "SVP & Chief Financial Officer (CFO)", "company": "DXP Enterprises, Inc."} }, { "name": "company-name-in-title-headline-beats-stale-toplevel", "profile": { "jobTitle": "Chief Financial Officer", "companyName": "The Spearhead Group Inc.", "headline": "Principal Legal Officer", "experiences": [ {"title": "The Spearhead Group Inc.", "companyName": "The Spearhead Group Inc.", "jobStartedOn": "2021-10", "jobStillWorking": true} ] }, "expected": {"title": "Principal Legal Officer", "company": "The Spearhead Group Inc."} }, { "name": "company-name-in-title-repaired-via-toplevel-when-no-headline", "profile": { "jobTitle": "Chief Financial Officer", "companyName": "The Spearhead Group Inc.", "experiences": [ {"title": "The Spearhead Group Inc.", "companyName": "The Spearhead Group Inc.", "jobStartedOn": "2021-10", "jobStillWorking": true} ] }, "expected": {"title": "Chief Financial Officer", "company": "The Spearhead Group Inc."} }, { "name": "company-name-in-title-headline-beats-stale-work-toplevel-nonfinance", "profile": { "jobTitle": "Software Engineer", "companyName": "Acme Cloud", "headline": "VP of Engineering @ Acme Cloud", "experiences": [ {"title": "Acme Cloud", "companyName": "Acme Cloud", "jobStartedOn": "2024-01", "jobStillWorking": true} ] }, "expected": {"title": "VP of Engineering", "company": "Acme Cloud"} }, { "name": "clean-current-cfo", "profile": { "jobTitle": "Chief Financial Officer", "companyName": "Subsea Environmental Services", "experiences": [ {"title": "Chief Financial Officer", "companyName": "Subsea Environmental Services", "jobStartedOn": "07-2025", "jobStillWorking": true}, {"title": "Chief Financial Officer", "companyName": "ClimeCo", "jobStartedOn": "09-2018", "jobEndedOn": "06-2025", "jobStillWorking": false} ] }, "expected": {"title": "Chief Financial Officer", "company": "Subsea Environmental Services", "role_kind": "work_current"} }, { "name": "nonfinance-stale-toplevel-engineering", "profile": { "jobTitle": "Software Engineer", "companyName": "Old Startup", "headline": "VP of Engineering", "experiences": [ {"title": "Software Engineer", "companyName": "Old Startup", "jobStartedOn": "2012", "jobEndedOn": "2016", "jobStillWorking": false}, {"title": "VP of Engineering", "companyName": "Acme Cloud", "jobStartedOn": "01-2024", "jobStillWorking": true} ] }, "expected": {"title": "VP of Engineering", "company": "Acme Cloud", "role_kind": "work_current"} }, { "name": "nonfinance-headline-repair-sales", "profile": { "jobTitle": "Globex Corporation", "companyName": "Globex Corporation", "headline": "Head of Sales @ Globex Corporation", "experiences": [ {"title": "Globex Corporation", "companyName": "Globex Corporation", "jobStartedOn": "2023-05", "jobStillWorking": true} ] }, "expected": {"title": "Head of Sales", "company": "Globex Corporation"} }, { "name": "nonfinance-headline-repair-ops-newline", "profile": { "jobTitle": "Initech", "companyName": "Initech", "headline": "Director of Operations\nInitech", "experiences": [ {"title": "Initech", "companyName": "Initech", "jobStartedOn": "2022-09", "jobStillWorking": true} ] }, "expected": {"title": "Director of Operations", "company": "Initech"} }, { "name": "nonfinance-multirole-headline-disambiguated-by-target", "target_role": "engineering", "profile": { "jobTitle": "Hooli", "companyName": "Hooli", "headline": "Advisor at Several | VP Engineering at Hooli | Mentor", "experiences": [ {"title": "Hooli", "companyName": "Hooli", "jobStartedOn": "2021-01", "jobStillWorking": true} ] }, "expected": {"title": "VP Engineering", "company": "Hooli"} }, { "name": "target-role-must-not-match-company-substring", "target_role": "engineering", "profile": { "jobTitle": "Engineering Works LLC", "companyName": "Engineering Works LLC", "headline": "VP Sales @ Engineering Works", "experiences": [ {"title": "Engineering Works LLC", "companyName": "Engineering Works LLC", "jobStartedOn": "2023-01", "jobStillWorking": true} ] }, "expected": {"title": "VP Sales", "company": "Engineering Works LLC"} }, { "name": "real-dxp-multiline-headline-with-board-suffix-and-ticker", "profile": { "jobTitle": "Board Member", "companyName": "DXP Enterprises, Inc.", "headline": "SVP & Chief Financial Officer (CFO), Board Member \nDXP Enterprises, Inc.\n(Nasdaq: DXPE)", "experiences": [ {"title": "DXP Enterprises, Inc.", "companyName": "DXP Enterprises, Inc.", "jobStartedOn": "2021-04", "jobStillWorking": true} ] }, "expected": {"title": "SVP & Chief Financial Officer (CFO)", "company": "DXP Enterprises, Inc."} }, { "name": "target-role-must-skip-advisory-segment", "target_role": "engineering", "profile": { "jobTitle": "Acme", "companyName": "Acme", "headline": "Engineering Advisor | VP Engineering | Acme", "experiences": [ {"title": "Acme", "companyName": "Acme", "jobStartedOn": "2022-06", "jobStillWorking": true} ] }, "expected": {"title": "VP Engineering", "company": "Acme"} }, { "name": "advisor-is-work-when-target-role-expects-advisor", "target_role": "security advisor", "profile": { "jobTitle": "Security Advisor", "companyName": "SecureCo", "headline": "Security Advisor at SecureCo", "experiences": [ {"title": "Security Advisor", "companyName": "SecureCo", "jobStartedOn": "2025-02", "jobStillWorking": true} ] }, "expected": {"title": "Security Advisor", "company": "SecureCo", "role_kind": "work_current"} }, { "name": "advisor-remains-nonwork-without-target-role-context", "profile": { "jobTitle": "Advisor", "companyName": "Several Startups", "headline": "Advisor to founders", "experiences": [ {"title": "Advisor", "companyName": "Several Startups", "jobStartedOn": "2024-01", "jobStillWorking": true} ] }, "expected": {"title": "Advisor", "company": "Several Startups", "role_kind": "nonwork_only_current"} }, { "name": "string-false-jobStillWorking-must-not-beat-real-current", "profile": { "jobTitle": "VP Sales", "companyName": "NewCo", "experiences": [ {"title": "VP Sales", "companyName": "OldCo", "jobStartedOn": "2025-01", "jobStillWorking": "false", "jobEndedOn": "2025-06"}, {"title": "Chief Revenue Officer", "companyName": "NewCo", "jobStartedOn": "2025-07", "jobStillWorking": "true"} ] }, "expected": {"title": "Chief Revenue Officer", "company": "NewCo", "role_kind": "work_current"} }, { "name": "malformed-none-profile-returns-empty-no-crash", "profile": null, "expected": {"title": "", "company": "", "role_kind": "none"} }, { "name": "malformed-nondict-experiences-falls-back-to-toplevel", "profile": { "jobTitle": "Controller", "companyName": "Widgets Inc", "experiences": "not-an-array", "headline": "Controller at Widgets Inc" }, "expected": {"title": "Controller", "company": "Widgets Inc", "role_kind": "toplevel_only"} }, { "name": "malformed-nondict-experience-entry-skipped", "profile": { "jobTitle": "Office Manager", "companyName": "Localshop", "experiences": [ null, "garbage", {"title": "Office Manager", "companyName": "Localshop", "jobStartedOn": "2023-03", "jobStillWorking": true} ] }, "expected": {"title": "Office Manager", "company": "Localshop", "role_kind": "work_current"} } ] -
fixtures_name_validation.json 6.8 KB
[ { "source_first": "Ajay", "source_last": "Uppaluri", "profile_name": "Julie Ho MBA CPA", "expected_match": false }, { "source_first": "Alli", "source_last": "Reiss", "profile_name": "Katie Gillespie (Alvadj)", "expected_match": false }, { "source_first": "Anders", "source_last": "Krohn", "profile_name": "Samir Das", "expected_match": false }, { "source_first": "Annie", "source_last": "Weezorak", "profile_name": "Juston Warthen", "expected_match": false }, { "source_first": "Bear", "source_last": "Sumner", "profile_name": "Kanwal (Pervaz) Ibrahim", "expected_match": false }, { "source_first": "Bryant", "source_last": "Przybilla", "profile_name": "Sarah Gonzalez Krupa", "expected_match": false }, { "source_first": "Chad", "source_last": "Trabucco", "profile_name": "Jacky Poulos", "expected_match": false }, { "source_first": "Christopher", "source_last": "Bywaletz", "profile_name": "Clay Slaughter", "expected_match": false }, { "source_first": "Cormac", "source_last": "McGuire", "profile_name": "Ryan Goodwin", "expected_match": false }, { "source_first": "Dan", "source_last": "Ensslen", "profile_name": "Bradd Wildstein", "expected_match": false }, { "source_first": "Dan", "source_last": "Grossberg", "profile_name": "Ayush Pradip", "expected_match": false }, { "source_first": "Doug", "source_last": "Sechrist", "profile_name": "Lena Waters", "expected_match": false }, { "source_first": "Inna", "source_last": "Ra", "profile_name": "J. Eduardo Inarra", "expected_match": false }, { "source_first": "Jared", "source_last": "Barol", "profile_name": "Howie Rothstein", "expected_match": false }, { "source_first": "J", "source_last": "Erwin", "profile_name": "Brayden J. Erwin", "expected_match": false }, { "source_first": "Jesse", "source_last": "Endo", "profile_name": "Meenakshi Mahey Kumar", "expected_match": false }, { "source_first": "John", "source_last": "Queally", "profile_name": "Sipa Mbaye", "expected_match": false }, { "source_first": "Kat", "source_last": "Dao", "profile_name": "Kelly Hendrickson", "expected_match": false }, { "source_first": "Kumbi", "source_last": "Murinda", "profile_name": "\u2601 Joshua Sangster", "expected_match": false }, { "source_first": "Liz", "source_last": "Christo", "profile_name": "Haley Vechell", "expected_match": false }, { "source_first": "Marcc", "source_last": "Bedine", "profile_name": "Luben Solev", "expected_match": false }, { "source_first": "Matt", "source_last": "Vegliante", "profile_name": "Darnell Newman", "expected_match": false }, { "source_first": "Maureen", "source_last": "Newsome", "profile_name": "Maureen McBride", "expected_match": false }, { "source_first": "Nathan", "source_last": "Feder", "profile_name": "Vaughn Sulit", "expected_match": false }, { "source_first": "Noelle", "source_last": "Carlson", "profile_name": "Adam Wrobel", "expected_match": false }, { "source_first": "Rae", "source_last": "Guimond", "profile_name": "Brian Newsom", "expected_match": false }, { "source_first": "Sara Rose", "source_last": "Harcus", "profile_name": "Josh Harcus", "expected_match": false }, { "source_first": "Shaun", "source_last": "Preston-Walsh", "profile_name": "Katie Kentner Swick", "expected_match": false }, { "source_first": "Tejesh", "source_last": "Chotalia", "profile_name": "Alistair Crooks", "expected_match": false }, { "source_first": "Thomas", "source_last": "Amegadzie", "profile_name": "Steven Snell", "expected_match": false }, { "source_first": "Romy", "source_last": "Pabiot", "profile_name": "Romy P.", "expected_match": true }, { "source_first": "Andrew", "source_last": "Kim", "profile_name": "Andrew Kim", "expected_match": true }, { "source_first": "James", "source_last": "Roth", "profile_name": "James Roth", "expected_match": true }, { "source_first": "Alex", "source_last": "Carstens", "profile_name": "Alex Carstens", "expected_match": true }, { "source_first": "Amanda", "source_last": "Snell", "profile_name": "Amanda Snell", "expected_match": true }, { "source_first": "Oleksandr", "source_last": "Rekov", "profile_name": "Alexander Rekov", "expected_match": true }, { "source_first": "Rocky", "source_last": "Katz", "profile_name": "Yerachmiel 'Rocky' Katz", "expected_match": true }, { "source_first": "J Ryan", "source_last": "Williams", "profile_name": "J. Ryan Williams", "expected_match": true }, { "source_first": "Emiliano", "source_last": "Rodr\u00edguez", "profile_name": "Emiliano Rodriguez", "expected_match": true }, { "source_first": "Suzanna", "source_last": "ODonohue", "profile_name": "Suzanna O'Donohue", "expected_match": true }, { "source_first": "Mike", "source_last": "Smith", "profile_name": "Michael Smith", "expected_match": true }, { "source_first": "Dan", "source_last": "Jones", "profile_name": "Daniel Jones", "expected_match": true }, { "source_first": "Bill", "source_last": "Lee", "profile_name": "William Lee", "expected_match": true }, { "source_first": "Liz", "source_last": "Chen", "profile_name": "Elizabeth Chen", "expected_match": true }, { "source_first": "Chris", "source_last": "Park", "profile_name": "Christopher Park", "expected_match": true }, { "source_first": "Matt", "source_last": "Davis", "profile_name": "Matthew Davis", "expected_match": true }, { "source_first": "Tom", "source_last": "Wilson", "profile_name": "Thomas Wilson", "expected_match": true }, { "source_first": "Alex", "source_last": "Taylor", "profile_name": "Alexander Taylor", "expected_match": true }, { "source_first": "Andy", "source_last": "Brown", "profile_name": "Andrew Brown", "expected_match": true }, { "source_first": "Nate", "source_last": "Green", "profile_name": "Nathan Green", "expected_match": true }, { "source_first": "Shaun", "source_last": "Preston-Walsh", "profile_name": "Shaun Walsh", "expected_match": true }, { "source_first": "Rob", "source_last": "Martinez", "profile_name": "Robert Martinez", "expected_match": true } ] -
flatten-search-contact-persons.py 3.3 KB
#!/usr/bin/env python3 """ Flatten deepline_native_search_contact output into one CSV row per person. Use after a play run whose contact column contains the persisted search_contact result. The source CSV is never modified. """ import argparse import csv import json import sys def _load_cell(value): if not value: return {} if isinstance(value, dict): return value try: parsed = json.loads(value) except Exception: return {} return parsed if isinstance(parsed, dict) else {} def _get_path(obj, path): cur = obj for key in path: if not isinstance(cur, dict): return None cur = cur.get(key) return cur def _persons_from_cell(value): cell = _load_cell(value) candidates = [ ["result", "data", "output", "persons"], ["result", "output", "persons"], ["data", "output", "persons"], ["output", "persons"], ["persons"], ] for path in candidates: persons = _get_path(cell, path) if isinstance(persons, list): return [p for p in persons if isinstance(p, dict)] return [] def _first(value, *keys): for key in keys: if isinstance(value, dict) and value.get(key): return value[key] return "" def flatten(rows, contacts_col): out = [] for row in rows: persons = _persons_from_cell(row.get(contacts_col)) for person in persons: full_name = _first(person, "name", "full_name") first_name = _first(person, "first_name", "firstName") last_name = _first(person, "last_name", "lastName") if not full_name: full_name = " ".join(part for part in [first_name, last_name] if part) out.append( { "company_name": row.get("company_name", ""), "domain": row.get("domain") or row.get("company_domain", ""), "contact_name": full_name, "first_name": first_name, "last_name": last_name, "title": _first(person, "title", "job_title", "jobTitle"), "linkedin_url": _first(person, "linkedin_url", "linkedin", "linkedinUrl"), "seniority": _first(person, "seniority"), "department": _first(person, "department"), "matched_titles": row.get("matched_titles", ""), "source_provider": "deepline_native_search_contact", } ) return out def main(): parser = argparse.ArgumentParser() parser.add_argument("csv_path") parser.add_argument("--contacts-col", default="contacts") args = parser.parse_args() with open(args.csv_path, newline="", encoding="utf-8") as handle: rows = list(csv.DictReader(handle)) fieldnames = [ "company_name", "domain", "contact_name", "first_name", "last_name", "title", "linkedin_url", "seniority", "department", "matched_titles", "source_provider", ] writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames) writer.writeheader() writer.writerows(flatten(rows, args.contacts_col)) if __name__ == "__main__": main() -
select-current-role.py 12.2 KB
#!/usr/bin/env python3 """ Select a person's CURRENT primary WORK role from a LinkedIn scrape. Why this exists: the top-level `jobTitle` from LinkedIn scrapers is often a stale or secondary entry (an old job, or a concurrent board seat). The reliable current role is the most-recently-started ACTIVE work experience, with board/advisory/ charity/retired excluded when a real operating role also exists, and the company-name-in-title scrape artifact repaired. Usage: # From a CSV whose `li_scrape` column holds the raw scraper JSON python3 select-current-role.py enriched.csv --scrape-col li_scrape \ --out-title current_title --out-company current_company --out-start job_start # Eval against fixtures python3 select-current-role.py --fixtures fixtures_current_role.json """ import argparse import csv import json import re import sys NONWORK_RE = re.compile( r"\b(board\s+member|member\s+board\s+of\s+directors?|board\s+of\s+directors?|" r"of\s+the\s+board|chair(?:man|person)?\s+of\s+the\s+board|advisor|advisory|" r"trustee|volunteer|mentor|charit|foundation|non-?profit|self-?employed|" r"council\s+member|committee\s+member|emeritus|retired|ambassador)\b", re.I, ) NONPROFIT_ORG_RE = re.compile( r"\b(foundation|charit|trees\s+for|habitat|rotary|united\s+way|church|ministr|non-?profit)\b", re.I, ) # A LinkedIn headline is usually "<Role> @ <Company>" / "<Role> at <Company>" / # "<Role> | <Company>" / "<Role> - <Company>", sometimes with the company on a second # line. The role is the leading segment before the first company separator/newline. # This is role-AGNOSTIC on purpose: it recovers a CFO, a VP Engineering, a Head of # Sales, an Office Manager, whatever the headline leads with. HEADLINE_SPLIT_RE = re.compile( r"\s*(?:@|\bat\b|\||\u00b7|\u2014|\u2013|\n|\r|\s-\s)\s*", re.I, ) def _norm(s): return re.sub(r"[^a-z0-9]", "", (s or "").lower()) def _truthy(v): """Coerce a possibly-stringified boolean. Scrapers serialize jobStillWorking as a real bool, or as the STRING "false"/"true"/"0"/"1"/"no"/"yes". Python's bool("false") is True, so a naive cast marks a long-ended role as current and it can beat the real current role. Normalize before trusting it.""" if isinstance(v, str): return v.strip().lower() in ("true", "1", "yes", "y", "t") return bool(v) def is_nonwork(title, company, target_role=None): title_norm = _norm(title) target_norm = _norm(target_role) target_is_advisor_role = "advisor" in target_norm or "advisory" in target_norm if target_is_advisor_role and title_norm and (target_norm in title_norm or title_norm in target_norm): # "Security Advisor" is a real target role when the campaign asks for # security advisors. Generic "Advisor" still counts as non-work when no # target-role context is supplied, and "Engineering Advisor" still loses # to "VP Engineering" for an engineering campaign. return bool(NONPROFIT_ORG_RE.search(company or "")) blob = f"{title or ''} {company or ''}" return bool(NONWORK_RE.search(blob)) or bool(NONPROFIT_ORG_RE.search(company or "")) def _start_key(e): """Parse 'MM-YYYY' / 'YYYY-MM' / 'YYYY' -> sortable int. Missing -> 0.""" s = str(e.get("start") or e.get("jobStartedOn") or "") m = re.match(r"(\d{1,2})-(\d{4})$", s) if m: return int(m.group(2)) * 100 + int(m.group(1)) m = re.match(r"(\d{4})-(\d{2})", s) if m: return int(m.group(1)) * 100 + int(m.group(2)) m = re.search(r"(\d{4})", s) return int(m.group(1)) * 100 if m else 0 def _experiences(profile): """Normalize the experiences array from either scraper shape.""" out = [] if not isinstance(profile, dict): return out raw = profile.get("experiences") or profile.get("experience") or [] if not isinstance(raw, list): return out for e in raw: if not isinstance(e, dict): continue title = (e.get("title") or e.get("position") or "").strip() company = (e.get("companyName") or e.get("company") or "").strip() sd = e.get("jobStartedOn") or e.get("startDate") start = sd.get("text") if isinstance(sd, dict) else (sd or "") end = e.get("jobEndedOn") or e.get("endDate") or "" current = _truthy(e.get("jobStillWorking")) if "jobStillWorking" in e else not end out.append({"title": title, "company": company, "start": start, "start_key": _start_key({"start": start}), "end": end, "current": current}) return out def _role_from_headline(headline, company, target_role=None): """Recover a role title from a 'Role @ Company' headline, role-agnostically. If target_role is given (the campaign's target function, e.g. 'engineering', 'sales', 'finance'), prefer a headline segment containing it; otherwise take the leading segment that isn't the company name.""" headline = (headline or "").strip() if not headline: return None segs = [s.strip(" .,-|·").strip() for s in HEADLINE_SPLIT_RE.split(headline) if s.strip()] # Drop any segment that IS the company or is contained in it (or contains it): # "Engineering Works" vs company "Engineering Works LLC" is the company, not a role. cn = _norm(company) def _is_company(s): ns = _norm(s) return bool(ns) and bool(cn) and (ns == cn or ns in cn or cn in ns) segs = [s for s in segs if s and not _is_company(s)] if not segs: return None # Trim each segment's trailing non-work appositive ("CFO, Board Member" -> "CFO") # BEFORE deciding if the segment is itself a non-work seat. Otherwise a real role # with a board suffix gets wrongly discarded and we fall through to junk like a # "(Nasdaq: TICKER)" line. Drop segments that are *purely* non-work or empty. trimmed = [(_trim_role(s), s) for s in segs] work = [t for (t, _orig) in trimmed if t and not is_nonwork(t, "", target_role)] if target_role: # Prefer a work segment that mentions the target role; never return a # board/advisory/charity segment just because it contains the role word # ("Engineering Advisor" must lose to "VP Engineering"). for t in work: if re.search(re.escape(target_role), t, re.I): return t if work: return work[0] # Nothing reads as a work role: fall back to the first trimmed segment, or None. return next((t for (t, _o) in trimmed if t), None) def _trim_role(seg): """Drop a trailing comma-appositive when it's a non-work secondary role. 'SVP & Chief Financial Officer (CFO), Board Member' -> 'SVP & Chief Financial Officer (CFO)'. Leaves real compound titles ('VP Finance, Treasury') intact.""" parts = [p.strip() for p in seg.split(",")] while len(parts) > 1 and is_nonwork(parts[-1], ""): parts.pop() return ", ".join(parts).strip() def select_current_role(profile, target_role=None): """Return dict: {title, company, start, role_kind}. role_kind in work_current / nonwork_only_current / work_recent_no_current / nonwork_recent / none. target_role (optional): the campaign's target function, used only to disambiguate a company-name-in-title repair from a multi-role headline. Role-agnostic without it.""" if not isinstance(profile, dict): # Malformed/None/non-dict input (a stray null cell, a list, a string) must not # crash a 10k-row run. Return an empty role rather than raising. return {"title": "", "company": "", "start": "", "role_kind": "none"} exps = _experiences(profile) current = [e for e in exps if e["current"]] cur_work = [e for e in current if not is_nonwork(e["title"], e["company"], target_role)] chosen, kind = None, "none" if cur_work: chosen, kind = max(cur_work, key=lambda e: e["start_key"]), "work_current" elif current: chosen, kind = max(current, key=lambda e: e["start_key"]), "nonwork_only_current" else: work = [e for e in exps if not is_nonwork(e["title"], e["company"], target_role)] if work: chosen, kind = max(work, key=lambda e: e["start_key"]), "work_recent_no_current" elif exps: chosen, kind = max(exps, key=lambda e: e["start_key"]), "nonwork_recent" if not chosen: # last resort: top-level fields t = (profile.get("jobTitle") or "").strip() c = (profile.get("companyName") or "").strip() return {"title": t, "company": c, "start": profile.get("jobStartedOn", ""), "role_kind": "none" if not t else "toplevel_only"} title, company, start = chosen["title"], chosen["company"], chosen["start"] # Repair company-name-in-title artifact. The whole discipline distrusts the # top-level `jobTitle` (it's often a stale/secondary entry), so prefer the # HEADLINE first (the person's own current-role summary) and fall back to the # top-level title only when the headline yields nothing usable. Doing it the # other way round returns a decade-old "Software Engineer" over a current # "VP Engineering" that the headline plainly states. if title and company and _norm(title) == _norm(company): recovered = _role_from_headline(profile.get("headline"), company, target_role) if recovered and _norm(recovered) != _norm(company): title = recovered else: top = (profile.get("jobTitle") or "").strip() if top and not is_nonwork(top, company, target_role) and _norm(top) != _norm(company): title = top return {"title": title, "company": company, "start": start, "role_kind": kind} def _load_scrape(cell): """Unwrap a CSV cell that may be a {"result":{"data":[...]}} envelope or raw JSON.""" try: j = json.loads(cell) except Exception: return None res = j.get("result", j) if isinstance(j, dict) else j data = res.get("data") if isinstance(res, dict) else None if isinstance(data, list) and data: data = data[0] if isinstance(data, dict): return data return res if isinstance(res, dict) and ("experiences" in res or "experience" in res or "jobTitle" in res) else None def run_fixtures(path): cases = json.load(open(path)) passed = failed = 0 for c in cases: got = select_current_role(c["profile"], c.get("target_role")) exp = c["expected"] ok = (_norm(got["title"]) == _norm(exp.get("title", "")) and (not exp.get("company") or _norm(got["company"]) == _norm(exp["company"]))) if exp.get("role_kind"): ok = ok and got["role_kind"] == exp["role_kind"] status = "PASS" if ok else "FAIL" if ok: passed += 1 else: failed += 1 print(f" {status} [{c.get('name','?')}] expected {exp} got {got}") print(f"\n{passed} passed, {failed} failed") sys.exit(1 if failed else 0) def main(): ap = argparse.ArgumentParser() ap.add_argument("csv", nargs="?") ap.add_argument("--scrape-col", default="li_scrape") ap.add_argument("--out-title", default="current_title") ap.add_argument("--out-company", default="current_company") ap.add_argument("--out-start", default="job_start") ap.add_argument("--out-kind", default="role_kind") ap.add_argument("--target-role", default=None, help="optional campaign target function (e.g. 'engineering', 'sales', " "'finance') to disambiguate a multi-role headline during title repair") ap.add_argument("--fixtures") a = ap.parse_args() if a.fixtures: run_fixtures(a.fixtures) return csv.field_size_limit(10 ** 7) rows = list(csv.DictReader(open(a.csv))) fields = list(rows[0].keys()) if rows else [] for col in (a.out_title, a.out_company, a.out_start, a.out_kind): if col not in fields: fields.append(col) for r in rows: prof = _load_scrape(r.get(a.scrape_col, "")) res = select_current_role(prof, a.target_role) if prof else {"title": "", "company": "", "start": "", "role_kind": "none"} r[a.out_title], r[a.out_company], r[a.out_start], r[a.out_kind] = ( res["title"], res["company"], res["start"], res["role_kind"]) w = csv.DictWriter(sys.stdout, fieldnames=fields) w.writeheader() w.writerows(rows) if __name__ == "__main__": main() -
validate-emails.py 2 KB
#!/usr/bin/env python3 """Validate enriched email data against company domains. Usage: python3 ~/.claude/skills/deepline-gtm/scripts/validate-emails.py enriched.csv \ --email-col email --domain-col domain Flags rows where the email domain doesn't match the company domain. Catches previous-employer or wrong-contact emails. Read-only — the input CSV is never modified. """ import argparse import csv import sys def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('csv_file', help='Path to the CSV file to validate.') parser.add_argument('--email-col', required=True, help='Email column name.') parser.add_argument('--domain-col', required=True, help='Domain column name.') parser.add_argument('--name-col', default='full_name', help='Name column for display (default: full_name).') args = parser.parse_args() with open(args.csv_file) as f: rows = list(csv.DictReader(f)) if not rows: print('No rows found.') return headers = set(rows[0].keys()) for flag, col in [('--email-col', args.email_col), ('--domain-col', args.domain_col)]: if col not in headers: print(f"Error: column '{col}' not found. Available: {sorted(headers)}", file=sys.stderr) sys.exit(1) mismatches = [] for r in rows: email = r.get(args.email_col, '') domain = r.get(args.domain_col, '') if email and domain and email.split('@')[-1] != domain: mismatches.append(r) print(f"MISMATCH: {r.get(args.name_col, '?')} — {r.get(args.email_col)} vs {r.get(args.domain_col)}") total = len(rows) n = len(mismatches) rate = (n / total * 100) if total else 0 print(f"\n{n}/{total} rows mismatched ({rate:.0f}%)") if rate > 20: print("WARNING: >20% mismatch — contact-finding step may need re-running with better disambiguation.") if __name__ == '__main__': main() -
validate-linkedin-names.py 10.1 KB
#!/usr/bin/env python3 """ Validate LinkedIn profile names against source names. Usage: # Validate a CSV with source names vs scraped profile names python validate-linkedin-names.py enriched.csv \ --source-first first_name --source-last last_name \ --profile-name-col profile_data.full_name # Run against fixture file for eval python validate-linkedin-names.py --fixtures fixtures_name_validation.json """ import argparse import csv import json import re import sys import unicodedata _QUOTED_NICK_RE = re.compile(r"['\"](\w+)['\"]") _CLEAN_NAME_RE = re.compile(r"[^\w\s'-]") _NORMALIZE_RE = re.compile(r"[^a-z\s-]") NICKNAMES = { "mike": {"michael"}, "michael": {"mike"}, "bob": {"robert", "rob"}, "robert": {"bob", "rob"}, "rob": {"robert", "bob"}, "bill": {"william", "will"}, "william": {"bill", "will"}, "will": {"william", "bill"}, "liz": {"elizabeth", "beth"}, "elizabeth": {"liz", "beth"}, "beth": {"elizabeth", "liz"}, "jim": {"james", "jimmy"}, "james": {"jim", "jimmy"}, "jimmy": {"james", "jim"}, "joe": {"joseph"}, "joseph": {"joe"}, "dan": {"daniel", "danny"}, "daniel": {"dan", "danny"}, "danny": {"daniel", "dan"}, "dave": {"david"}, "david": {"dave"}, "chris": {"christopher"}, "christopher": {"chris"}, "matt": {"matthew"}, "matthew": {"matt"}, "tom": {"thomas"}, "thomas": {"tom"}, "tony": {"anthony"}, "anthony": {"tony"}, "nick": {"nicholas", "nico"}, "nicholas": {"nick", "nico"}, "rick": {"richard"}, "richard": {"rick", "dick"}, "dick": {"richard"}, "steve": {"steven", "stephen"}, "steven": {"steve", "stephen"}, "stephen": {"steve", "steven"}, "andy": {"andrew", "drew"}, "andrew": {"andy", "drew"}, "drew": {"andrew", "andy"}, "alex": {"alexander", "oleksandr", "aleksandr"}, "alexander": {"alex", "oleksandr"}, "oleksandr": {"alex", "alexander"}, "sam": {"samuel", "samantha"}, "samuel": {"sam"}, "samantha": {"sam"}, "ben": {"benjamin", "benny"}, "benjamin": {"ben", "benny"}, "jon": {"jonathan", "john"}, "jonathan": {"jon", "john"}, "john": {"jon", "jonathan"}, "ed": {"edward", "ted"}, "edward": {"ed", "ted"}, "ted": {"edward", "theodore"}, "theodore": {"ted"}, "pat": {"patrick", "patricia"}, "patrick": {"pat"}, "patricia": {"pat"}, "kate": {"katherine", "katie", "kathy"}, "katherine": {"kate", "katie", "kathy"}, "jen": {"jennifer", "jenny"}, "jennifer": {"jen", "jenny"}, "sara": {"sarah"}, "sarah": {"sara"}, "meg": {"megan"}, "megan": {"meg"}, "mandy": {"amanda"}, "amanda": {"mandy"}, "ron": {"ronald"}, "ronald": {"ron"}, "charlie": {"charles", "chuck"}, "charles": {"charlie", "chuck"}, "greg": {"gregory"}, "gregory": {"greg"}, "jeff": {"jeffrey"}, "jeffrey": {"jeff"}, "doug": {"douglas"}, "douglas": {"doug"}, "nate": {"nathan", "nathaniel"}, "nathan": {"nate"}, "nathaniel": {"nate"}, "zach": {"zachary"}, "zachary": {"zach"}, "max": {"maxwell", "maximilian"}, "maxwell": {"max"}, "maximilian": {"max"}, "kat": {"katherine", "kate", "kathy"}, } def normalize(s): """Strip accents, lowercase, remove non-alpha except hyphens.""" s = unicodedata.normalize("NFD", s) s = "".join(c for c in s if unicodedata.category(c) != "Mn") s = _NORMALIZE_RE.sub("", s.lower().strip()) return s.strip() def first_names_match(source, profile): sf = normalize(source) pf = normalize(profile) if not sf or not pf: return False, "empty" if sf == pf: return True, "exact" if len(sf) >= 3 and (sf.startswith(pf) or pf.startswith(sf)): return True, "prefix" # Nickname sf_variants = {sf} | NICKNAMES.get(sf, set()) pf_variants = {pf} | NICKNAMES.get(pf, set()) if sf_variants & pf_variants: return True, "nickname" # Single initial if len(sf) == 1 and pf.startswith(sf): return True, "initial" if len(pf) == 1 and sf.startswith(pf): return True, "initial" # Check if source contains profile name (handles "J Ryan" matching "J") source_parts = sf.split() if len(source_parts) > 1: for part in source_parts: if part == pf or (len(part) >= 3 and (part.startswith(pf) or pf.startswith(part))): return True, "multi_part" part_variants = {part} | NICKNAMES.get(part, set()) if part_variants & pf_variants: return True, "multi_part_nickname" # Check if profile contains quoted nickname nickname_match = _QUOTED_NICK_RE.search(profile.lower()) if nickname_match: nick = nickname_match.group(1) if nick == sf or sf in NICKNAMES.get(nick, set()) or nick in NICKNAMES.get(sf, set()): return True, "quoted_nickname" return False, "mismatch" def last_names_match(source, profile): sl = normalize(source) pl = normalize(profile) if not sl or not pl: return False, "empty" if sl == pl: return True, "exact" # Hyphenated: any part matches sl_parts = set(sl.replace("-", " ").split()) pl_parts = set(pl.replace("-", " ").split()) if sl_parts & pl_parts: return True, "hyphenated" # One contains the other (covers single-char initials too: "P" in "pabiot") if sl in pl or pl in sl: return True, "substring" return False, "mismatch" def validate_name(source_first, source_last, profile_full_name): """Returns (match: bool, details: dict).""" profile_clean = _CLEAN_NAME_RE.sub("", profile_full_name).strip() parts = profile_clean.split() if len(parts) < 2: return False, {"reason": "profile_name_too_short", "profile_clean": profile_clean} profile_first = parts[0] profile_last = " ".join(parts[1:]) first_ok, first_reason = first_names_match(source_first, profile_first) # Also check if source first name appears as a quoted nickname anywhere in profile if not first_ok: nickname_match = _QUOTED_NICK_RE.search(profile_clean.lower()) if nickname_match: nick = normalize(nickname_match.group(1)) sf = normalize(source_first) if nick == sf or sf in NICKNAMES.get(nick, set()) or nick in NICKNAMES.get(sf, set()): first_ok, first_reason = True, "quoted_nickname" last_ok, last_reason = last_names_match(source_last, profile_last) return first_ok and last_ok, { "first_match": first_ok, "first_reason": first_reason, "last_match": last_ok, "last_reason": last_reason, "profile_first": profile_first, "profile_last": profile_last, } def run_fixtures(fixture_path): """Run eval against fixture file. Returns exit code.""" with open(fixture_path) as f: fixtures = json.load(f) results = [] failures = [] for fix in fixtures: match, details = validate_name( fix["source_first"], fix["source_last"], fix["profile_name"] ) expected = fix["expected_match"] results.append((expected, match)) if match != expected: failures.append({ "source": f"{fix['source_first']} {fix['source_last']}", "profile": fix["profile_name"], "expected": expected, "got": match, "details": details, }) passed = sum(1 for e, m in results if e == m) total = len(results) print(f"Name validation eval: {passed}/{total} passed ({passed/total*100:.0f}%)") if failures: print(f"\nFailures ({len(failures)}):") for fail in failures: icon = "FP" if fail["got"] else "FN" print(f" [{icon}] {fail['source']} -> {fail['profile']}") print(f" expected={fail['expected']}, got={fail['got']}, {fail['details']}") tp = sum(1 for e, m in results if e and m) fp = sum(1 for e, m in results if not e and m) fn = sum(1 for e, m in results if e and not m) tn = sum(1 for e, m in results if not e and not m) precision = tp / (tp + fp) if (tp + fp) > 0 else 0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0 print(f"\nConfusion matrix: TP={tp} FP={fp} FN={fn} TN={tn}") print(f"Precision: {precision:.2f} (of accepted, how many correct)") print(f"Recall: {recall:.2f} (of correct, how many accepted)") print(f"F1: {f1:.2f}") # Thresholds if precision < 0.95: print(f"\nFAIL: Precision {precision:.2f} < 0.95 threshold") return 1 if recall < 0.85: print(f"\nFAIL: Recall {recall:.2f} < 0.85 threshold") return 1 print("\nPASS: All thresholds met (precision >= 0.95, recall >= 0.85)") return 0 def run_csv(csv_path, source_first_col, source_last_col, profile_name_col): """Validate a CSV and print mismatches.""" csv.field_size_limit(10_000_000) with open(csv_path) as f: rows = list(csv.DictReader(f)) matched = 0 mismatched = 0 skipped = 0 for r in rows: sf = r.get(source_first_col, "").strip() sl = r.get(source_last_col, "").strip() pn = r.get(profile_name_col, "").strip() if not sf or not sl or not pn: skipped += 1 continue ok, details = validate_name(sf, sl, pn) if ok: matched += 1 else: mismatched += 1 print(f" MISMATCH: {sf} {sl} -> {pn} ({details})") total = matched + mismatched print(f"\nValidated: {matched}/{total} matched, {mismatched} mismatched, {skipped} skipped") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Validate LinkedIn profile names") parser.add_argument("csv", nargs="?", help="CSV file to validate") parser.add_argument("--fixtures", help="Run eval against fixture JSON file") parser.add_argument("--source-first", default="first_name") parser.add_argument("--source-last", default="last_name") parser.add_argument("--profile-name-col", default="profile_name") args = parser.parse_args() if args.fixtures: sys.exit(run_fixtures(args.fixtures)) elif args.csv: run_csv(args.csv, args.source_first, args.source_last, args.profile_name_col) else: parser.print_help()
-
-
.recipe-template 2 KB · in bundle
-
claude-deepline-statusline.mjs 24.8 KB · in bundle
-
enriching-and-researching.md 32.4 KB
# Enriching and Researching (JTBD Draft) Use this doc for row-level enrichment, research, waterfalls, validation, coalescing, and custom per-row transforms. This doc does **not** cover list building, source discovery, or TAM/provider scouting before you have rows. If you do not yet have a seed list, source URL, or known entities, stop and use `finding-companies-and-contacts.md`. ## Core rule If a play exists, run it with `deepline plays run`. Waterfall prebuilts run their whole provider cascade internally and stop on the first valid hit — when you use one, say so: state the play's provider order (from `deepline plays describe`) and point at the output's source column (`email_source`, `phone_source`) as the stop-on-found evidence. Never re-implement a waterfall a prebuilt already encodes. Every prebuilt below has a batch form that takes a CSV directly: ```bash deepline plays run prebuilt/name-and-domain-to-email-waterfall-batch --input '{"csv":"leads.csv"}' deepline runs export <run-id> --out leads_with_emails.csv ``` Column names differ from the play's defaults? Pass a `columns` map from play field to CSV header — check `deepline plays describe prebuilt/<name>` for the required fields and default column map: ```bash deepline plays run prebuilt/name-and-domain-to-email-waterfall-batch \ --input '{"csv":"leads.csv","columns":{"first_name":"fname","last_name":"lname","domain":"company_domain"}}' ``` Discover plays with `deepline plays search <query>`; use `deepline plays list --show-cost` for available cost estimates and `deepline plays describe <name>` for contracts. The same `--show-cost` option on `plays share publish` or `plays share update` controls public-page cost metrics. Do not hardcode a provider list a play already encodes. Use something else only when: - a prebuilt is close but not exact → fork it (`deepline plays get prebuilt/<name> --source --out ./<name>.play.ts`, then `plays check`) or wrap it (`plays bootstrap ... --using play:prebuilt/<name>`) - no play exists → author one per [recipes/deepline-plays.md](recipes/deepline-plays.md) - you are testing a niche provider path → direct `deepline tools execute` `deepline enrich` is deprecated; do not reach for it — the sections below are all plays. Billing recovery: if `deepline billing balance` or any paid Deepline command reports zero credits, `no_billing`, or an insufficient-credits failure, stop paid work and ask the user whether they want to add Deepline credits. If the response includes a `recovery` object, quote `recovery.top_up_command` and `recovery.checkout_command` exactly in your answer, including `--json` and `--no-open`. Do not shorten them, and do not run either command until the user explicitly approves. ## Scenario table Every play named in this table runs via `deepline plays run prebuilt/<name>` (batch: `prebuilt/<name>-batch`) — never through `enrich --with`. | Scenario | Use when | Default play/tool | Why | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Name + domain -> work email | You have name + domain (or can resolve domain from company_name / Sales Nav URL first) | `Name + domain -> work email` | Canonical deterministic path. Handles both direct and domain-first-then-waterfall cases. | | LinkedIn URL -> work email | Standard `/in/` LinkedIn URL + name. `domain` optional; include if known for extra coverage. | `LinkedIn URL -> work email` | Works with or without domain. Do NOT use for SN `/sales/lead/` URLs — resolve domain first and use the name+domain play. | | Email -> person/company context | You have an inbound or work email and need person + company details | `Email -> person/company context` | Good for hydrating context from a single strong identifier. | | Personal email -> LinkedIn profile | Bare personal email (Gmail/GitHub signup); you need LinkedIn + name + company, not a work email | `Personal email -> LinkedIn profile` | Reverse identity resolution; best for personal-email-only lists. | | Company -> persona lookup | You have an account and need candidate contacts by role or seniority | `Company -> persona lookup` | Canonical play for company-to-persona lookup | | Company name only -> resolve domain first | You need to recover homepage/domain before downstream enrichment | `Company name only -> resolve domain first` | Domain lookup is mechanical and should not start with `deeplineagent` | | Validate a recovered email | An email lookup has already run | `Notes` | Validation belongs after recovery or coalescing, not before | | Manual email waterfall | You need custom provider order or play customization | `Manual email waterfall` | Lets you control ordering and spend | | Find a LinkedIn URL for a known person | You have name, domain, and role context | `Notes` | Cheap deterministic lookup when the query is specific | | Pull rich LinkedIn or work-history data | The URL is already known and you need structured profile data | `Notes` | Structured output beats ad hoc web synthesis | | Find a mobile phone number | A verified person identity already exists | `Notes` | Best later in the pipeline after identity is strong | | Mechanical company enrichment | You need direct structured account data | `Notes` | Cheaper and cleaner, often more accurate than `deeplineagent` for firmographics | | Coalesce competing provider outputs | Multiple columns target the same field | `Notes` | Deterministic canonicalization after parallel providers | | Per-row factual account research | You need custom research or synthesis that provider fields do not cover | `Custom enrichment with run_javascript and deeplineagent` | Use `deeplineagent` for AI work and `run_javascript` for deterministic transforms | | Research pass before writing | You need company or person research to support later copy | `Custom enrichment with run_javascript and deeplineagent` | Research belongs here and should feed a later writing step | | Generate copy after research | The research column already exists and you now need messaging, first lines, scoring copy, or sequence text | `writing-outreach.md` | Copywriting should route to the outreach doc, usually with `deeplineagent` once the research column exists | | LinkedIn post URL -> list of engagers | You have a LinkedIn post URL and want all reactors/commenters | `linkedin_post_to_engagers` | Scrape all reactors/commenters from a LinkedIn post. Returns structured engager list. | | List of people with name + position -> ICP qualification | You have person rows with name and headline and need tier classification | `engagers_to_icp_qualification` | Classify leads against ICP using headline/position via deeplineagent | | **Personal email discovery** | User explicitly asks for personal emails (Gmail, Hotmail, etc.) - NOT work emails | `Personal email discovery` | Use Fullenrich or BetterContact. Do not substitute work-email providers. | ## Notes - **Personal vs work emails:** When the user asks for personal emails, they mean Gmail/Hotmail/Yahoo, not work emails. Use Fullenrich (`contact.personal_emails`) or BetterContact; do not substitute Hunter, LeadMagic, or other work-email providers. - Direct provider tools are preferred for mechanical fields when no play exists. - When multiple providers recover the same mechanical field, prefer the route that bills on returned results or successful hits. Use request-priced, page-priced, or broad AI passes only after a tiny pilot proves they return usable rows. - `run_javascript` is for deterministic transforms, normalization, coalescing, templating, and cheap row-level glue logic. - `deeplineagent` is the default AI path for research, synthesis, custom signals, and classification when JS is not enough. - Domain lookup / homepage recovery is mechanical. Use `exa_search` with rich context or `serper_google_search`, not `deeplineagent`. - For local SMB or restaurant contact emails, do not start with name + domain work-email waterfalls unless you have a named person. Prefer the small-business prospecting recipe first: Maps identity, website/contact extraction, then optional Facebook/Instagram profile contact fields when the row or pilot suggests social profiles are the best public source. ScrapeCreators profile tools are candidate routes, not required steps. - Persona lookup means "find candidate contacts at a company for a target role or seniority." Use the dedicated play, not generic research. - Validate after recovery or coalescing, not during each waterfall step. - For contact-to-email work, route by your strongest identifiers: name + domain -> `Name + domain -> work email` (or `First + last + domain -> work email`); name + company only (no domain) OR Sales Navigator contacts -> resolve domain first, then `name-and-domain-to-email-waterfall`; standard `/in/` LinkedIn URL + name -> `LinkedIn URL -> work email` (domain optional). - **Sales Navigator exports**: `linkedin_url` values in `/sales/lead/` format are rejected by every provider (dropleads, crustdata, deepline_native, PDL). Do not pass them directly to any email waterfall. Resolve the company domain first, then use `name-and-domain-to-email-waterfall`. - Contacts from a people search (e.g. dropleads_search_people) with **standard `/in/`** URLs -> `person-linkedin-to-email` (`domain` optional). Does NOT apply to SN `/sales/lead/` URLs. - Validation interpretation: `valid` is deliverable, `catch_all` is usable but riskier, `invalid` should be dropped, and `unknown` is unresolved. - Phone recovery usually comes later in the pipeline than email or LinkedIn recovery. - Prefer inline code for short `run_javascript` transforms. Only move code into files when the logic is long, reused, or too awkward to keep inline. - In Claude Desktop on Windows, the working directory may look like `C:\Users\...` while the tool executor is still Bash/Git Bash. Use Bash commands such as `rm`, not PowerShell commands such as `Remove-Item`, unless the session context explicitly says the active shell is PowerShell. ## Plays ### Name + domain -> work email Play tool: `name-and-domain-to-email-waterfall` **Required payload:** `first_name`, `last_name`, `domain`. `company_name` is not part of the payload. **Routing by what you have:** | You have | Action | | --------------------------------------------------------- | ------------------------------------------------- | | name + domain | Use the play directly | | name + company_name (no domain) or SN `/sales/lead/` URLs | Resolve domain first (below), then use the play | | standard `/in/` LinkedIn URL + name | Skip this play — use `LinkedIn URL -> work email` | **Play internals.** Runs common validated patterns first; only `valid` hits count. Falls through to `dropleads_email_finder -> hunter_email_finder -> leadmagic_email_finder -> crustdata_v3_person_search -> peopledatalabs_enrich_contact`. `catch_all` is usable for outreach but not an automatic win inside the play. **Example:** ```bash # One contact deepline plays run prebuilt/name-and-domain-to-email-waterfall \ --input '{"first_name":"Ada","last_name":"Lovelace","domain":"acme.com"}' # A CSV — pilot on a slice first, then the full file, then EXPORT TO THE REQUESTED PATH head -3 leads.csv > pilot.csv deepline plays run prebuilt/name-and-domain-to-email-waterfall-batch --input '{"csv":"pilot.csv"}' deepline runs export <pilot-run-id> --out pilot_out.csv # inspect quality + cost deepline plays run prebuilt/name-and-domain-to-email-waterfall-batch --input '{"csv":"leads.csv"}' deepline runs export <full-run-id> --out "$FINAL_CSV" # the deliverable — never skip this ``` A user-stated scope is already approved (SKILL.md §4.1): after the pilot checks out, run the full file and export to `$FINAL_CSV` without stopping to ask. For a small stated input (≤ ~25 rows), skip the slice entirely and run the full file once. The pilot is never the deliverable. **Domain-first resolution** — when you only have `company_name` or a SN `/sales/lead/` URL, resolve domains before the email play. For a handful of companies, resolve each directly and patch the CSV: ```bash deepline tools execute exa_search --input '{"query":"Acme Corp official website","numResults":1}' ``` For a list, author a two-column custom play per [recipes/deepline-plays.md](recipes/deepline-plays.md) — `exa_search` column, then a `run_javascript` column extracting the registrable domain — and feed its export to `prebuilt/name-and-domain-to-email-waterfall-batch`. ### LinkedIn URL -> work email Play tool: `person-linkedin-to-email` **Required payload:** `linkedin_url`. Use when contacts have a **standard `/in/`** LinkedIn URL (e.g. from `dropleads_search_people`). The play works off the LinkedIn URL directly. **Do NOT use for Sales Navigator `/sales/lead/` URLs** — providers reject them. Resolve the company domain first, then use the name+domain play above. **Example:** ```bash deepline plays run prebuilt/person-linkedin-to-email --input '{"linkedin_url":"https://www.linkedin.com/in/example/"}' # CSV: pilot a slice, then the full file deepline plays run prebuilt/person-linkedin-to-email-batch --input '{"csv":"contacts.csv"}' deepline runs export <run-id> --out contacts_with_emails.csv ``` ### Email -> person/company context Play tool: `deepline_native_enrich_contact` Why this play: - Email is a strong identifier; use it directly. - This is hydration, not research. Example: ```bash deepline tools execute deepline_native_enrich_contact --input '{"email":"ada@acme.com"}' ``` For a CSV of inbound emails, author a one-column custom play calling the same tool per row ([recipes/deepline-plays.md](recipes/deepline-plays.md)). ### Personal email -> LinkedIn profile Play tool: `personal-email-to-linkedin`. Required payload: `personal_email` only (name/company unknown, unlike the work-email plays). Use it when a signup list has only personal emails and you want to know who they are. Returns `linkedin_url`, `name`, `company`, `title`; a profile is often more recoverable and useful than a work email here. The play normalizes Gmail first, then waterfalls `deepline_native` -> `forager` -> `findymail` -> `peopledatalabs`, charging per hit. The same play runs two ways: ```bash deepline plays run prebuilt/personal-email-to-linkedin --input '{"personal_email":"ada@gmail.com"}' # CSV of signups deepline plays run prebuilt/personal-email-to-linkedin-batch --input '{"csv":"signups.csv"}' deepline runs export <run-id> --out signups_with_profiles.csv ``` Bare personal email coverage is ~25-40%, so over-provision. If a row returns a company but no work email, chain `name-and-domain-to-email-waterfall-batch` on the export. ### Contact identity -> phone Play tool: `person-to-phone` Why this play: - Use it when you already know the person identity and want the highest-signal phone lookup order. - Cost-optimized: starts with the cheapest providers and escalates to expensive ones only as fallbacks. - All providers charge only on successful hit (post_deduct), so total cost scales with coverage, not attempts. - Follow up with `trestle_phone_validation` to verify line type, carrier, and activity score before outbound. Play details: - Required inputs are `first_name`, `last_name`, and `domain`. - `email` and `linkedin_url` are optional hints that unlock additional provider paths. - The play handles the phone provider order internally. Treat the play as the source of truth for exact sequencing. - LeadMagic runs in two gated forms inside the play: LinkedIn-based when `linkedin_url` exists, and email-based when `email` exists. - Use async aggregators (BetterContact, FullEnrich) as manual enrichment steps outside the play when the native waterfall misses. Example: ```bash deepline plays run prebuilt/person-to-phone \ --input '{"first_name":"Ada","last_name":"Lovelace","domain":"acme.com","email":"ada@acme.com","linkedin_url":"https://www.linkedin.com/in/example/"}' # CSV: pilot a slice, then the full file deepline plays run prebuilt/person-to-phone-batch --input '{"csv":"contacts.csv"}' deepline runs export <run-id> --out contacts_with_phones.csv ``` ### Company -> persona lookup Play tool: `company-to-contact` Why this play: - This is the canonical company-to-persona play when you have a company domain. - Use it for both role-targeted and seniority-targeted contact discovery. - The right default for prompts like "find GTM engineers at these companies". - Prefer exact title tokens in `roles` when the user intent is specific, for example `CEO`, `Founder`, `CTO`, `CMO`, `VP Marketing`, `Head of Security`, `Director of Engineering`, `RevOps`. - Use broader functional roles only when the user intent is genuinely broad, for example `marketing`, `security`, `finance`, `product`, `engineering`, `sales`, `growth`. Broad roles are useful, but they are noisier and often return adjacent titles. - A good default is 1-3 exact titles, or a broad function plus a strong level hint if exact titles are not known. - `seniority` is a first-class input, but it is only a level hint. Use portable values like `C-Level`, `Founder`, `VP`, `Head`, `Director`, `Manager`, `Senior`, `Entry`, `Intern`. Do not send raw provider enums like `c_level` unless you are bypassing the play and calling a provider directly. - Do not assume the play will invent hidden row-level provider fields for you. For interpolated CSV runs, `roles` and `seniority` pass through exactly as provided. - Clean contract: pass a company domain. If you only have a LinkedIn company URL, resolve the domain first before using this play. Provider behavior: - `dropleads` is strongest with exact title tokens. - `deepline_native` translates portable roles into provider-safe title filters, especially for leadership intent like `CEO`, `Founder`, `CTO`, `VP Marketing`, `Head of Security`, or `Director of Engineering`. - Exact-title provider search should not be the only source for founder/exec startup cases. - `icypeas` is a strong exact-profile fallback, especially for founders and startup operators. - `prospeo` and `crustdata` are structured fallbacks, not reasons to jump to `deeplineagent`. - For a very specific persona with only a broad function, refine the role phrasing before adding providers. Persona matching: - Treat requested `roles` and `seniority` as semantic intent, not raw substring rules. Provider search can return adjacent titles that contain the same words but mean something different. - Validate that the returned title actually matches the requested persona before treating it as the decision maker. If the match is weak, return no result, broaden intentionally, or mark it low confidence instead of filling the row with a plausible-looking person. - Common false positives: `Owner` can mean process/product owner, `Sales` can mean Salesforce, `Chief` can mean Chief of Staff, and `Security` can mean physical security. - Prefer exact title families or explicit role phrases when intent is narrow. For example, use `Founder`, `Co-Founder`, `CEO`, `Chief Executive Officer`, or `Owner/Proprietor` for business-owner intent instead of relying on a loose `owner` token. - Ambiguous terms need supporting evidence from company/domain fit, full title context, and the requested function. Do not let one overlapping word override a bad persona fit. Operational rule: - If you only have `company_name`, resolve the domain first, then run persona lookup. - Do not use `deeplineagent` as the first pass for persona lookup. - Use `deeplineagent` only as a fallback research pass when the play and direct providers miss. - If provider results are weak or sparse, first re-check the available people/company search tools with category searches, then use Apify if you need a broader employee list. Category searches: - Use `people_search` when you need better title- and LinkedIn-oriented contact search options. - Use `company_search` when you need stronger company identity resolution or company-level inputs before the people search. Search examples: ```bash deepline tools search --categories people_search --search_terms "title filters,linkedin" deepline tools search --categories company_search --search_terms "structured filters,firmographics" ``` Example: ```bash deepline plays run prebuilt/company-to-contact \ --input '{"domain":"acme.com","roles":["VP Marketing"],"seniority":"VP"}' # CSV of accounts: pilot a slice, then the full file deepline plays run prebuilt/company-to-contact-batch --input '{"csv":"accounts.csv"}' deepline runs export <run-id> --out accounts_with_contacts.csv ``` Use the native prebuilt for repeatable domain-to-roster work: ```bash deepline plays describe prebuilt/company-domain-to-linkedin-employees-harvestapi deepline plays run prebuilt/company-domain-to-linkedin-employees-harvestapi \ --input '{"domain":"openai.com","max_items":25}' ``` Use the direct operations below only when you need a custom result shape: ```bash deepline tools describe harvestapi_get_company --schema-only deepline tools execute harvestapi_get_company --input '{"url":"https://www.linkedin.com/company/openai/"}' --json deepline tools describe harvestapi_search_leads --schema-only deepline tools execute harvestapi_search_leads --input '{"currentCompanies":"https://www.linkedin.com/company/openai/","sessionId":"STABLE_RANDOM_SESSION_ID","page":1}' --out openai-employees.csv ``` Generate the stable random `sessionId` before page 1 and reuse it on every page. Because HarvestAPI matches `currentCompanies` by company name, keep only results whose `currentPositions[].companyId` matches the target `element.id` returned by `harvestapi_get_company`. ### LinkedIn post URL -> list of engagers Use the native HarvestAPI prebuilt. It fetches both reactors and commenters, paginates each operation, unions their `elements` by actor identity, and returns the established engager-row schema: ```bash deepline plays describe prebuilt/linkedin-post-to-engagers-harvestapi deepline plays run prebuilt/linkedin-post-to-engagers-harvestapi \ --input '{"post_url":"https://www.linkedin.com/posts/...","max_items":1000}' ``` Call the native operations directly only when you need a custom result shape: ```bash deepline tools describe harvestapi_get_post_reactions deepline tools describe harvestapi_get_post_comments deepline tools execute harvestapi_get_post_reactions --input '{"post":"https://www.linkedin.com/posts/...","page":1}' --out post-reactions.csv deepline tools execute harvestapi_get_post_comments --input '{"post":"https://www.linkedin.com/posts/...","page":1}' --out post-comments.csv ``` ### List of people with name + position -> ICP qualification Play tool: `engagers_to_icp_qualification` Classifies a person against an ICP using name + position/headline. Returns `{icp_tier, icp_reason}`. Do NOT use if qualification needs company size, funding, or web research — use a custom `deeplineagent` prompt instead. ```bash deepline plays run prebuilt/engagers-to-icp-qualification \ --input '{"first_name":"Ada","last_name":"Lovelace","position":"VP Engineering at Acme","icp_description":"Tier 1: VP/Head of Engineering, CTO at B2B SaaS. Tier 2: Senior engineers. Tier 3: everyone else."}' ``` For a CSV of engagers, wrap the tool in a small custom play mapping over the rows ([recipes/deepline-plays.md](recipes/deepline-plays.md)). ### Company name only -> resolve domain first Problem category: domain lookup / homepage recovery. Input profile: `company_name` plus any contextual hints you already have. Output target: canonical `domain` or homepage for downstream plays. Default tools: `exa_search` or `serper_google_search` Why this play: - Domain lookup is mechanical. - It should happen before persona lookup, email recovery, or company enrichment. - `deeplineagent` is the wrong default here because this is a search-and-resolve task, not a synthesis task. Routing rule: 1. Resolve domain/homepage with `exa_search` or `serper_google_search`. 2. Run the downstream play using the recovered domain. 3. Only use `deeplineagent` if provider/search outputs still do not cover the factual need and you need tool-backed reasoning to resolve the ambiguity. Example: ```bash deepline tools execute serper_google_search --input '{"query":"\"Acme Corp\" official site","num":5}' ``` For a list of companies, author a two-column custom play (search + extract) per [recipes/deepline-plays.md](recipes/deepline-plays.md). ### Custom email waterfall Problem category: custom provider ordering or custom extraction behavior. Use only when no native play fits, or you need to deliberately customize provider order. Fork the nearest prebuilt and edit its step order: ```bash deepline plays get prebuilt/name-and-domain-to-email-waterfall --source --out ./email-waterfall.play.ts # edit: drop/reorder legs, change gating deepline plays check ./email-waterfall.play.ts deepline plays run --file ./email-waterfall.play.ts --input '{"first_name":"Ada","last_name":"Lovelace","domain":"acme.com"}' ``` If `plays check` fails on a missing local import, that prebuilt is multi-file: wrap it with `plays bootstrap ... --using play:prebuilt/<name>` instead, or author the waterfall fresh per [recipes/deepline-plays.md](recipes/deepline-plays.md) (a `steps()` cascade: sequential legs, stop on first valid hit, validation after recovery). Rules that carry over from the native plays: pilot before scale; do not run email waterfalls without minimum match data (name + company, name + domain, or a strong LinkedIn-seeded identity); validation belongs after recovery, and the cost-aware plays only accept pattern hits the validator marks `valid`. ## Post-run validation After a play run, validate data quality before moving to the next phase. Run read-only checks — never modify the enriched CSV during validation. ```bash # Email domain vs company domain — catches previous-employer or wrong-contact emails python3 ~/.claude/skills/deepline-gtm/scripts/validate-emails.py enriched.csv \ --email-col email --domain-col domain ``` Flag mismatches; if >20% of rows mismatch, rerun contact finding with better company disambiguation. ```bash # LinkedIn name validation — catches wrong-person matches from search-based lookup python3 ~/.claude/skills/deepline-gtm/scripts/validate-linkedin-names.py enriched.csv \ --source-first first_name --source-last last_name --profile-name-col profile_name ``` Null out LinkedIn URLs where names don't match. ```bash # Current role extraction. Selects latest active work role and repairs artifacts. python3 ~/.claude/skills/deepline-gtm/scripts/select-current-role.py enriched.csv \ --scrape-col li_scrape --out-title current_title --out-company current_company ``` Do not trust top-level `jobTitle`; old roles or board/advisor entries can outrank the real current job. ```bash # Final contact audit. Projects delivery gates into ACTION + flag_reason. python3 ~/.claude/skills/deepline-gtm/scripts/contact-accuracy-audit.py final.csv \ > final_audited.csv ``` **For any contact list you will actually send to**, read [references/contact-accuracy.md](references/contact-accuracy.md). It gives the full workflow: resolve the current work role, confirm identity, catch job-changers, validate email independently, preserve lineage, discover current role-holders company-first when accounts are known, audit the final file, and deliver one `ACTION` plus `flag_reason` per row. ## Custom columns are plays Open-ended factual research, Claygent-style enrichment, custom signals, multi-source columns, personalization inputs: author a custom play per [recipes/deepline-plays.md](recipes/deepline-plays.md) — a dataset over your CSV with one `withColumn` per field. Routing inside the play: - `run_javascript` for deterministic row logic: formatting, normalization, coalescing, templating, parsing, conditional transforms. - `deeplineagent` for AI work: classification, extraction, scoring, structured generation, browsing, multi-step synthesis. Keep outputs structured with `jsonSchema` when a later column consumes them. - Split research and generation into separate columns; keep research here and route copywriting to `writing-outreach.md`. - Start prompts from [`prompts.json`](prompts.json): list keys with `jq -r 'keys[]' .skills/deepline-gtm/prompts.json`, print one with `jq -r '."<key>"' ...`, adapt it into the `deeplineagent` column's prompt. - Reading tool output inside a play: use the documented getters and `extracted*` accessors from `deepline tools describe <tool>` before drilling into raw provider nesting. The iterate loop applies with force here: run the play on 2-3 rows, read the per-column outcomes in the storage table, fix prompts/providers, then scale. ## Working directory (guardrail) **NEVER write to `/tmp/` or any absolute temp directory** — files in `/tmp/` are wiped on reboot and users have lost paid enrichment outputs. Set up a project-local WORKDIR with a task-descriptive slug (e.g. `deepline/data/acme-email-waterfall`) as step zero. See SKILL.md §3.2 for the full rule. ```bash WORKDIR="deepline/data/<descriptive-slug>" && mkdir -p "$WORKDIR" && echo "$WORKDIR" ``` ## Exit back to discovery If you realize the task is actually: - "find the companies first" - "find the candidate contacts first" - "where does this data source live?" Stop and route to `finding-companies-and-contacts.md`. This doc assumes you already have rows or known entities. -
finding-companies-and-contacts.md 44.2 KB
# Finding Companies and Contacts (JTBD Draft) Use this doc for discovery, sourcing, TAM/list building, known-source extraction, contact discovery, and hiring-qualified company search before any row-level enrichment. This doc does **not** cover email waterfalls, row-level play mechanics, coalescing, validation, or personalization columns. If you already have rows and need to fill or transform columns, stop and use `enriching-and-researching.md`. ## Core rules Default to discovery/search here. The moment the work becomes per-row enrichment, hand off to `enriching-and-researching.md`. **Companies first, then people.** When the task involves finding contacts at companies matching criteria (ICP, portfolio, accelerator, hiring signal), always discover the company set first, then search for people at those companies. Do not start with people-search tools (`exa_people_search`, `dropleads_search_people`, etc.) using broad title+industry queries — you will get noisy, unaffiliated results. The only exception is when the user provides a specific named company list and only needs contacts. Use a list-building/search subagent for non-trivial multi-provider discovery. Tell subagents to read this file; keep small obvious lookups inline. Subagent output contract: - return a seed CSV or structured list only - preserve source lineage - stop before row-level enrichment - recommend the next step Search-to-enrichment handoff rules: - stop adding ad-hoc row-level scripts once you have a seed list - move per-column work to a play per `enriching-and-researching.md` - keep lineage in-sheet with `_metadata` ## Company search providers (ROI order) Escalate only when you need a filter the current step lacks. 1. **`free_simple_company_search`** — SQL over the free company corpus. Exact/domain lookup and bounded SQL. FREE. 2. **`dropleads_search_people`** — adds revenue, funding range, technologies + people filters. Use `dropleads_get_lead_count` to size first. FREE. 3. **`crustdata_v3_company_search`** — adds investors, funding stage, and structured filters. Use `crustdata_v3_company_search_autocomplete` (free) first. **When DB providers return 0** (pre-revenue startups, niche verticals, non-US): use `exa_company_search` for concept search, `parallel_extract` for known source URLs, `serper_google_maps_search` for local/SMB, and `serper_google_search` for scoped directory discovery. ## Tool discovery Use `deepline tools search` once near the top when the scenario is clear but the exact tool family is not. Provide an intent query, or omit it only when `--categories` or `--search_terms` supplies the structured search; both filters accept comma-separated values. Provider names belong in the query, not in a `--prefix` flag. Prefer category-constrained searches. More search terms helps with recall. Then inspect the strongest candidates. ```bash deepline tools search --categories company_search --search_terms "structured filters,firmographics" & deepline tools search --categories people_search --search_terms "title filters,linkedin" & deepline tools search --categories company_search --search_terms "investors,funding" & deepline tools search --categories research --search_terms "ads,technographics" & wait deepline tools describe crustdata_v3_company_search & deepline tools describe dropleads_search_people & deepline tools describe apify_run_actor_sync & wait ``` After tool discovery, shortlist 1-2 candidates, inspect schemas, validate enum-like inputs, then run a narrow first pass. ## People search providers (ROI order) 1. **`wiza_search_prospects`** — 30 masked results, no contact data. Good for sizing. FREE. 2. **`dropleads_search_people`** — workhorse: title, seniority, dept, geo, tech, revenue. Near-zero coverage <50 emp. FREE. 3. **`crustdata_v3_person_search`** — cheap bulk paid discovery. Use `crustdata_v3_person_search_autocomplete` (free) first. 4. **`lusha_search_contacts`** — dept, seniority, industry, title, tech filters. 5. **`ai_ark_people_search`** — title, seniority, skills, location, company attributes. **Alts:** `exa_people_search` (tiny startups); `contactout_search_people`; `icypeas_find_people` (700M+ DB); `rocketreach_search_people` (30+ filters). ## Discovery workflow | Step | What to do | Why | | ---- | ---------------------------------------------------------- | ----------------------------------------------- | | 0 | Check if the data already exists or has a known source URL | Avoid unnecessary provider calls | | 1 | Shortlist 1-2 providers from the reference table | Prevent random provider thrash | | 2 | Inspect the schema with `deepline tools describe` | Avoid guessed field names and bad payloads | | 3 | Validate enum-like values with autocomplete tools | Prevent silent empty searches | | 4 | Execute a count-like or narrow first pass | Cheaply confirm fit before full pull | | 5 | Prefer result-priced routes when coverage is uncertain | Avoid paying per miss during exploratory fanout | Anti-patterns: - **jumping to people-search first** — searching for "GTM Engineer at YC startup" via `exa_people_search` or `dropleads_search_people` before having a company list. Find companies first, then find people at each. - reconstructing a known directory with repeated search queries - firing all providers in parallel before routing - guessing filter names or enum values - using `deeplineagent` as the default discovery path here. - continuing row-level logic / enrichment/research here after a seed list exists ## Scenario table | Scenario | Read Section | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------- | | Sizing an audience or validating market volume | `Search audiences` | | Companies matching a crisp ICP (funding, headcount, geo, vertical) | `Structured company search` | | Pulling from a known URL — portfolio, directory, registry, LinkedIn/Reddit/X, conference, filing | `Known-source extraction` | | Contacts for a CSV or existing company list (row-based) | Stop — route to `enriching-and-researching.md` | | Contacts for a few companies named in the prompt | `People search at known companies` | | Companies hiring for a role or function | `Hiring-qualified search` | | LinkedIn URL or company page recovery | `URL recovery` | | Niche path the default routes don't cover | `Tool discovery` (top of doc) | ## Search audiences Use when sizing reachability, volume, or market fit — "how many people can we reach?", "is this market big enough?", "pull 100k leads". **Count-first invariant:** prefer a dedicated count endpoint. Otherwise run with `limit:1` / `per_page:1` / `size:1` and read totals. Pull full pages only after shape + size look right. For large payloads or Windows/PowerShell quoting trouble, write the JSON to a file and pass `--payload @path/to/payload.json`; generated payloads can use `--payload-stdin`. ### Sizing a people audience Default to Dropleads first (strongest free first pass, LinkedIn-rich). Fall back to Wiza/Forager/Icypeas/Prospeo/PDL. ```bash deepline tools execute dropleads_get_lead_count --payload '{"filters":{"jobTitles":["CEO"],"industries":["Technology"]}}' deepline tools execute dropleads_search_people --payload '{"filters":{"jobTitles":["VP Sales"],"industries":["Technology"]},"pagination":{"page":1,"limit":1}}' deepline tools execute forager_person_role_search_totals --payload '{"role_title":"\"Software Engineer\""}' deepline tools execute icypeas_count_people --payload '{"query":{"currentJobTitle":{"include":["CTO"]}}}' deepline tools execute peopledatalabs_person_search --payload '{"query":{"bool":{"must":[{"term":{"location_country":"United States"}},{"term":{"job_title_role":"marketing"}}]}},"size":1}' ``` ### Sizing a company audience (ICP) Structured company path at `limit:1`, or dedicated totals. ```bash deepline tools execute crustdata_v3_company_search --payload '{"filters":{"op":"and","conditions":[{"field":"taxonomy.categories","type":"in","value":["Identity Management","Fraud Detection"]},{"field":"locations.country","type":"=","value":"USA"},{"field":"basic_info.employee_count_range","type":"in","value":["51-200","201-500"]},{"field":"funding.last_round_type","type":"in","value":["Series A","Series B"]}]},"limit":1}' deepline tools execute forager_organization_search_totals --payload '{"industries":[1]}' deepline tools execute prospeo_search_company --payload '{"company":{"names":{"include":["Intercom"]},"websites":{"include":["intercom.com"]}},"page":1}' ``` ### Sizing hiring demand Use when the question is "how many companies are hiring this role?" (job-market size). For hiring evidence on a known company set, use `crustdata_v3_job_search` in the Hiring-qualified section below. It costs 0.02 Deepline credits per returned result; empty result pages are free. ```bash deepline tools execute forager_job_search_totals --payload '{"title":"\"Sales Engineer\""}' deepline tools execute hunter_discover --payload '{"query":"B2B SaaS companies","limit":1}' ``` ### Rough domain-level signal Quick directional answer about a single company — not a persona-level audience estimate. Domain email volume ≠ reachable persona count. ```bash deepline tools execute hunter_email_count --payload '{"domain":"stripe.com"}' ``` ## Structured company search Use this section when the user has a crisp ICP, such as: - funding stage - headcount range - geography - vertical/category - investor - hiring proxy or company maturity Recommended course of action: 1. Use structured company search first. 2. Validate enum-like values before committing to a full search. 3. Run a count-like first pass with `limit:1` when appropriate. 4. Pull more rows than the final target if downstream attrition is expected. 5. If the exact filter set is unclear, use the tool-discovery pattern above instead of hardcoding a provider guess. 6. Stop after a tiny pilot when usable rows are sparse, domains are missing, taxonomy is broad, or cost per usable row is high. Change source route before scaling. ### Free native company search Use `free_simple_company_search` for exact and bounded SQL: - exact `normalized_domain = ...` or `normalized_domain IN (...)` - small exact `linkedin_url = ...` or `linkedin_url IN (...)` batches - small exact `company_name = ...` or `company_name IN (...)` batches - anchored prefix candidates like `company_name ILIKE 'acme%'` Plain `ILIKE '%...%'` is valid SQL, but can scan the full 35M-company table with long OR chains, `COUNT`/`GROUP BY`, broad locations, or high limits. Use purpose-built providers for broad keyword discovery, strict totals, live coverage, or native facets. ### Canonical value validation ```bash deepline tools execute crustdata_v3_company_search_autocomplete --payload '{"field":"taxonomy.categories","query":"identity","limit":5}' ``` ### Count-first structured company search ```bash deepline tools execute crustdata_v3_company_search --payload '{"filters":{"op":"and","conditions":[{"field":"taxonomy.categories","type":"in","value":["Identity Management","Fraud Detection"]},{"field":"locations.country","type":"=","value":"USA"},{"field":"basic_info.employee_count_range","type":"in","value":["51-200","201-500"]},{"field":"funding.last_round_type","type":"in","value":["Series A","Series B"]}]},"limit":1}' ``` ### Full structured company pull ```bash deepline tools execute crustdata_v3_company_search --payload '{"filters":{"op":"and","conditions":[{"field":"taxonomy.categories","type":"in","value":["Identity Management","Fraud Detection"]},{"field":"locations.country","type":"=","value":"USA"},{"field":"basic_info.employee_count_range","type":"in","value":["51-200","201-500"]},{"field":"funding.last_round_type","type":"in","value":["Series A","Series B"]}]},"sorts":[{"field":"headcount.latest_count","order":"desc"}],"limit":35}' ``` Structured company search is the wrong choice when: - the user gave you a known source page - the target is too fuzzy/conceptual for structured filters - you need semantic discovery first, not a precise market pull ## Known-source extraction (web and provider APIs) Use when the value lives on a public page you can fetch directly — VC portfolios, accelerator batches, conference sites, partner directories, SEC/EDGAR, registries, team pages, Reddit threads, LinkedIn/X profiles. Extractive, not discovery. Rule: if you have the URL, scrape it directly. Use search only to find the URL when the source itself is unknown. Prefer official pages over reconstructed lists. For investor-backed targeting ("companies backed by a16z", "YC W26"), official portfolio pages beat structured search. Source-type routing: - **Static HTML pages, registries, official filings** → `curl` or `WebFetch` (free). - **JS-rendered portfolios, directories, job boards** → `parallel_extract` (~1 cr). - **LinkedIn profiles, employees, posts, and reactions** → Native HarvestAPI operations. Search and describe the relevant `harvestapi_*` tool before execution. - **Reddit, X, Similarweb, or a LinkedIn surface HarvestAPI does not expose** → Apify actors. See [`portfolio-prospecting.md`](recipes/portfolio-prospecting.md) for investor/accelerator flow. Direct extraction example: ```bash deepline tools execute parallel_extract --payload '{"urls":["https://www.ycombinator.com/companies?batch=W26"],"objective":"Extract all company names, domains, and one-line descriptions from this page","full_content":true}' ``` For LinkedIn, prefer the native HarvestAPI provider: `harvestapi_get_profile` for one profile, `harvestapi_search_leads` for company employees, `harvestapi_get_profile_posts` for a profile's posts, and both `harvestapi_get_post_reactions` and `harvestapi_get_post_comments` for post engagers. Treat these names as starting hints. Tool search is broad and can be noisy, so confirm the exact operation with `deepline tools describe <operation> --schema-only` before execution. ```bash deepline tools describe harvestapi_get_profile --schema-only deepline tools execute harvestapi_get_profile --payload '{"url":"https://www.linkedin.com/in/someone/"}' --json ``` Use `--json` for single-object operations such as profile, company, and post lookups. Use `--out results.csv` for list operations such as employee, post, reaction, and comment searches; using `--out` on a single-object response can select an unrelated nested list for CSV preview. For post discovery, prefer a known `company`, `companyId`, `profile`, or `profileId` filter when available; broad keyword `search` is fuzzy and can include similarly named terms. Use Apify for source-specific scraping that HarvestAPI does not cover. Known non-HarvestAPI actors include `supreme_coder/linkedin-post` and `radeance/similarweb-scraper`; discover and inspect the current actor contract before execution. For LinkedIn URL recovery itself (not scraping after you have the URL), use `URL recovery` below. ## People search at known companies Use this section when the user already has target companies and needs candidate contacts. ### Resolve missing domains; do not make the user do it Company names are sufficient input for a known-company task. Before a domain-scoped contact or enrichment call, resolve the canonical domain for each named company yourself. Do not interrupt the task with a request for an "exact," "definitive," or comma-separated domain list. 1. Search the live catalog for a company/domain-resolution or web-search capability, then inspect its contract. Use a free or no-credit route for the first pass when one is available. 2. Search the company name with any supplied context (location, product, investor, LinkedIn URL, or person). Select a candidate only when its official page identifies the same organization. Do not use a directory, social profile, marketplace, or a search-result host as the company domain. 3. Normalize the resulting hostname, retain the official-page URL as `domain_evidence_url`, and carry `company_name`, `domain`, and `domain_confidence` into the next stage. 4. If the first route misses, try an independent company-search or web-search route before leaving the domain unresolved. Record the attempted routes and an explicit `domain_miss_reason`; never guess from the spelling of the name. For a common or ambiguous name, use the context already in the request and report the candidate and confidence in the normal output. Do not stop to ask the user to identify a domain unless choosing among live candidates would cause a material paid or external action. If no candidate can be verified, preserve that row as unresolved and continue with every other named company. Recommended course of action: 1. For nuanced roles or real titles at named companies, follow [`recipes/find-qualified-titles.md`](recipes/find-qualified-titles.md): `company_titles` -> qualify exact roster titles -> `deepline_native_search_contact` with `title_lists`. 2. Use `exa_people_search` after the roster/database pass to fill public-profile gaps. 3. Use `dropleads_search_people` afterward to add supplemental database rows or contact data. 4. Use broad function keywords plus seniority when no roster exists or the user wants broad audience sizing. 5. Prefer company domains over company names when you know them. 6. Stop at candidate contacts here. If the task becomes "fill in emails or enrich these rows", hand off to `enriching-and-researching.md`. ### Broad audience search and sizing DropLeads remains useful when the user wants a segment count, a broad sample, or coverage beyond the roster-qualified results. It is not the primary path for nuanced titles at a named company because keyword filters can miss titles such as "Director, Mount Sinai AI Assurance Lab." ```bash deepline tools execute dropleads_search_people --payload '{"filters":{"companyDomains":["stripe.com"],"jobTitles":["Growth","Sales","Revenue"],"seniority":["VP","Director"],"personalCountries":{"include":["United States"]}},"pagination":{"page":1,"limit":5}}' ``` ### Count-first people search ```bash deepline tools execute dropleads_search_people --payload '{"filters":{"jobTitles":["Marketing"],"seniority":["VP","Director"],"personalCountries":{"include":["United States"]}},"pagination":{"page":1,"limit":1}}' ``` ### Tiny-startup fallback exa_people_search returns keyword-matched LinkedIn profiles, not verified employees. For startups <50 people, check each title for the correct company; if >30% are unaffiliated, switch to `deeplineagent`. **Disambiguate common company names.** For names like "Ergo", "Bloom", or "Newton", add accelerator batch, domain, or product context so Exa does not return unrelated companies. ```bash # Bad — "Ergo" matches ERGO Direkt AG, ErgoPack, THE ERGO CORP, etc. deepline tools execute exa_people_search --payload '{"query":"GTM engineer at Ergo","numResults":5}' # Good — YC batch + domain disambiguates deepline tools execute exa_people_search --payload '{"query":"GTM engineer at Ergo joinergo.com YC W25","company_name":"Ergo","numResults":5}' ``` Use `company_name` to pass the target company as structured input — it appends to the query if not already present and tags the response meta for downstream validation. ## Role-based contact search **Do not guess exact job titles for broad people-search filters.** Titles vary wildly across companies, especially startups, so guessed exact-match filters miss adjacent real titles. - **Bad:** `jobTitles: ["Head of Growth", "VP RevOps", "GTM Engineer"]` — misses "Director of Growth Marketing", "Revenue Operations Lead", etc. - **Good:** `jobTitles: ["Growth"]` + `seniority: ["VP", "Director"]` — catches all growth-related senior roles via fuzzy matching. - **Best for known companies:** obtain verbatim titles from `company_titles`, qualify that roster, then pass the selected exact strings through `title_lists`. For broad searches without a roster, use 1-2 function keywords (Growth, Sales, Revenue, Security, Fraud, Identity, RevOps, Marketing) plus seniority. This works across DropLeads and CrustData. For <500-employee companies, narrow title filters often return 0; use broad keyword + seniority. Dropleads has near-zero coverage for <50-emp startups — switch to `exa_people_search` (see Tiny-startup fallback above). ## Hiring-qualified search Use this section when the user wants companies that are actively hiring for a specific role or likely need a specific function. Recommended course of action: 1. Discover the plausible company set first. If companies come from a known portfolio or accelerator (YC, a16z, etc.), extract the portfolio first via `Known-source extraction` — you'll get domains for free and skip domain-resolution. 2. Then qualify that set with hiring evidence. 3. Use public-job or semantic evidence only when structured hiring coverage is thin. 4. Treat hiring as a qualification layer, not the only discovery step. ### Structured hiring evidence for known companies ```bash deepline tools execute crustdata_v3_job_search --payload '{"filters":{"op":"and","conditions":[{"field":"company.basic_info.primary_domain","type":"in","value":["stripe.com","persona.id","sardine.ai"]}]},"limit":100}' ``` ### Public hiring evidence ```bash deepline tools execute exa_search --payload '{"query":"site:ycombinator.com \"GTM engineer\"","numResults":20,"type":"auto"}' ``` ## URL recovery Use this section when you already know the company or person identity and need the URL. Recommended course of action: 1. Use a highly specific query. 2. Include company and role context for people. 3. Leave null when the identity is not specific enough. 4. Only move to scraping once you already have the correct URL. ### Company LinkedIn URL lookup ```bash deepline tools execute serper_google_search --payload '{"query":"\"OpenAI\" site:linkedin.com/company","num":3}' ``` ### Person LinkedIn URL lookup ```bash deepline tools execute serper_google_search --payload '{"query":"\"Jane Smith\" \"Acme\" \"sales ops\" site:linkedin.com/in","num":5}' ``` ## Convergence rules | Rule | Guidance | | ----------------------------- | -------------------------------------------------------------------------- | | Filter, don't restart | Filter out bad matches and supplement gaps instead of restarting discovery | | Stop at good enough | If you have about 80% of the target after filtering, ship it | | Extract from search responses | Use provider-returned firmographics directly instead of re-enriching them | ## Provider reference | Tool | Best for | Server-side filters | Cost | Gotchas | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `curl` / `parallel_extract` / `WebFetch` | Data at known URLs: VC portfolios, accelerator directories, job boards, team pages, conference speaker lists | URL + optional CSS/objective | free (`curl`) or ~1 cr (`parallel_extract`) | Use `curl` for static HTML, `parallel_extract` for JS-rendered pages. One fetch usually returns the full dataset. | | `crustdata_v3_company_search` | Company lists with ICP constraints (funding, headcount, geography, industry) | funding stage, headcount range, country, categories, industry, employee growth, investor | 0.02 cr/result | Empty searches are free. Use `taxonomy.categories` for niche verticals; response already includes requested firmographics. | | `crustdata_v3_company_search_autocomplete` | Get canonical filter values before searching | — | free | Run before `company_search` for categorical fields. Requires non-empty `query`. | | `crustdata_v3_job_search` | Indexed job-listing discovery and hiring signals | company fields, title, location, job metadata | 0.02 cr/result | Empty searches are free. Use company-domain or company-id filters. Coverage is thinner on smaller companies; use employee-growth signals first when available. | | `crustdata_v3_person_search` | Structured person discovery | company domain, title, seniority, location, skills | 0.02 cr/result | Empty searches are free. Use V3 field paths and validate current employment before acting. | | `exa_search` | Concept-driven company/people discovery, gap-filling | semantic query only (no ICP filters) | ~5 cr with contents | Expect to discard 30-50%. `category:"company"` is incompatible with domain/text filters. | | `exa_people_search` | Contacts at small startups (<50 emp) | query string | ~1 cr/result | Returns structured entities. Use via a custom play column. | | `exa_agent` | Deep multi-source synthesis | outputSchema, multi-query | provider usage | Slow. Use for research, not list building. | | `dropleads_search_people` | People discovery + segmentation with structured filters | job titles, seniority, headcount, geography, keywords | free | Near-zero coverage for <50 emp startups. `keywords` must be split: `["GTM","Engineer"]` not `["GTM Engineer"]`. | | `dropleads_get_lead_count` | Sizing before full pull | same as search_people | free | — | | `serper_google_search` | URL discovery, `site:` scoped searches | query string | low-cost | Defaults `gl=us` and `hl=en`. Use `site:` + quoted phrases for precision. | | `parallel_search` | Broad discovery when you don't know which domains hold the data | objective string | ~1 cr | Lower precision than domain-scoped search. | | `parallel_extract` | URL-bound extraction, JS-rendered pages | URLs + objective | ~1 cr | Slow. Good for portfolio pages, job boards. | | `hunter_email_finder` | Email finding in waterfall | domain, first/last name | ~0.3 cr | Poor coverage for <50 emp companies. | | `peopledatalabs_company_search` | SQL-based company search | SQL (industry, size, funding, location) | expensive | Last resort. Exhaust others first. | | `crustdata_v3_person_enrich` | LinkedIn profile enrichment | LinkedIn URL | per matched record | Request only the field groups the workflow needs. | | `harvestapi_get_profile` / `harvestapi_search_leads` | LinkedIn profiles and company employees | profile URL or current-company filter | inspect live pricing | Prefer the native HarvestAPI provider; describe the selected operation before execution. | | `adyntel_facebook_ad_search` | Meta keyword-based ad search | keyword | ~1 cr | Additional channel coverage. | | `deeplineagent` | Tool-backed fallback research and ambiguity resolution | prompt + row context | varies | Use only after direct discovery paths fail or when you need guided synthesis over web findings. Ask for structured output with `jsonSchema`. | ## Sample calls by provider ### Serper Google Search Use `site:` plus quoted phrases for precision. Keyword soup without `site:` is noisy. ```bash # YC job listings for a specific role deepline tools execute serper_google_search --payload '{"query":"site:ycombinator.com \"GTM engineer\" \"Series B\"","num":10}' # Company LinkedIn URL discovery deepline tools execute serper_google_search --payload '{"query":"\"OpenAI\" site:linkedin.com/company","num":3}' ``` ### CrustData (company + person search, autocomplete) **Always read `packages/integrations/crustdata/` before building filter payloads.** Field names, enums, and operators are non-obvious. **Key rules:** autocomplete unknown canonical values; use `employee_count_range` for headcount filters and `employee_metrics.latest_count` only for sorts; `hq_country` uses ISO 3-letter codes; prefer `crunchbase_categories` for niche verticals; extract returned firmographics directly; use `employee_metrics.growth_6m_percent` before paid job search. **Operators**: `(.)` = fuzzy contains (default), `[.]` = substring, `=`, `!=`, `in`, `not_in`, `>`, `<`, `=>`, `=<`. ```bash # Always autocomplete first — compare crunchbase_categories vs linkedin_industries for your vertical deepline tools execute crustdata_v3_company_search_autocomplete --payload '{"field":"taxonomy.categories","query":"fraud","limit":5}' deepline tools execute crustdata_v3_company_search_autocomplete --payload '{"field":"taxonomy.professional_network_industry","query":"financial","limit":5}' ``` ```bash # Use crunchbase_categories for niche verticals, hq_country with ISO codes deepline tools execute crustdata_v3_company_search --payload '{"filters":{"op":"and","conditions":[{"field":"taxonomy.categories","type":"in","value":["Fraud Detection","Identity Management"]},{"field":"locations.country","type":"=","value":"USA"},{"field":"basic_info.employee_count_range","type":"in","value":["51-200","201-500"]},{"field":"funding.last_round_type","type":"in","value":["Series A","Series B"]}]},"sorts":[{"field":"headcount.latest_count","order":"desc"}],"limit":50}' ``` ### People Data Labs **PDL is expensive -- use it as a last resort.** Exhaust Exa, Google, and Crustdata first. **Shell quoting with PDL SQL:** PDL takes a raw SQL string. Avoid inline single-quote escaping in bash -- it breaks silently. Instead write the payload to a temp file and pass it with `--payload-file`, or use a bash heredoc: ```bash PAYLOAD=$(cat <<'EOF' {"sql": "SELECT * FROM company WHERE industry = 'financial services' AND location.country = 'united states' AND size IN ('51-200','201-500') AND latest_funding_stage IN ('series_a','series_b')", "size": 20} EOF ) deepline tools execute peopledatalabs_company_search --payload "$PAYLOAD" ``` ### Exa (search, answer, research) Exa is a semantic web index -- it finds pages by meaning, not just keywords. **Query rules:** Write natural-language descriptions, not keyword soup (`"B2B SaaS companies that sell sales automation tools"` not `"SaaS B2B sales tools 2025"`). Use `type: "neural"` (default) for concept-driven queries, `"deep"` with `additionalQueries` for broad coverage. Use `startPublishedDate`/`endPublishedDate` for recency. Use `contents.summary` for per-result LLM summaries, `contents.highlights` for snippets. **Critical: `category` vs `includeDomains` -- NOT interchangeable:** - `category: "company"` / `"people"` uses Exa's entity index. **`includeDomains`, `excludeDomains`, `includeText`, `excludeText` are NOT supported with `category`** -- throws an error. Use for "companies that _are_ X" (concept-driven), NOT "companies that _have_ X" (attribute-based). - `includeDomains` / `excludeDomains` -- scope a regular web search (no category) to specific sites. **"Companies that hire X role" -- use `includeDomains` on job boards, NOT `category:"company"`:** ```bash deepline tools execute exa_search --payload '{"query":"GTM engineer job opening at Y Combinator startup","numResults":15,"type":"neural","includeDomains":["ycombinator.com"],"contents":{"highlights":{"numSentences":2,"highlightsPerUrl":1}}}' ``` **Tool selection:** `exa_search` (general-purpose, start here), `exa_company_search` (category:"company" shorthand), `exa_people_search` (structured person entities, best as a play column), `exa_answer` (fact-checking only, low recall), `exa_agent` (deep multi-source, supports `outputSchema`). ```bash # Concept-based company search (category OK here) deepline tools execute exa_search --payload '{"query":"B2B SaaS companies building AI-powered sales tools","category":"company","numResults":10,"type":"neural","contents":{"summary":{"query":"What does this company do and what funding stage are they?"}}}' # Attribute + domain-scoped search (NO category -- use includeDomains instead) deepline tools execute exa_search --payload '{"query":"Series B fintech startups in New York","type":"neural","additionalQueries":["fintech companies Series B NYC"],"numResults":20,"includeDomains":["techcrunch.com","crunchbase.com"],"startPublishedDate":"2024-01-01T00:00:00Z","contents":{"summary":{"query":"What does this company do and what stage are they?"}}}' ``` ### Parallel (managed research) Good for broad discovery when you don't know which domains hold the data. Lower precision than domain-scoped Exa/Google but finds things others miss. Set `max_chars_total` > 10000 for 5+ results. ```bash deepline tools execute parallel_search --payload '{"mode":"agentic","objective":"Find recent hiring and launch signals for OpenAI","max_results":5,"excerpts":{"max_chars_per_result":1200,"max_chars_total":12000}}' ``` ## At-scale coverage completion Use this section when the job is coverage completion -- you already have target accounts/segments and need to backfill missing contacts/emails. ### Count-capable providers (verified) Use these when you want fast sizing before doing the full list pull. | Provider | Tool | Command | | ---------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dropleads | `dropleads_get_lead_count` | `deepline tools execute dropleads_get_lead_count --payload '{"filters":{"jobTitles":["CEO"],"industries":["Technology"]}}'` | | Dropleads | `dropleads_search_people` | `deepline tools execute dropleads_search_people --payload '{"filters":{"jobTitles":["VP Sales"],"industries":["Technology"]},"pagination":{"page":1,"limit":1}}'` | | Forager | `forager_organization_search_totals` | `deepline tools execute forager_organization_search_totals --payload '{"industries":[1]}'` | | Forager | `forager_job_search_totals` | `deepline tools execute forager_job_search_totals --payload '{"title":"\"Sales Engineer\""}'` | | Forager | `forager_person_role_search_totals` | `deepline tools execute forager_person_role_search_totals --payload '{"role_title":"\"Software Engineer\""}'` | | Icypeas | `icypeas_count_people` | `deepline tools execute icypeas_count_people --payload '{"query":{"currentJobTitle":{"include":["CTO"]}}}'` | | Prospeo | `prospeo_search_person` | `deepline tools execute prospeo_search_person --payload '{"person_job_title":{"include":["VP Sales"]},"page":1}'` | | Prospeo | `prospeo_search_company` | `deepline tools execute prospeo_search_company --payload '{"company":{"names":{"include":["Intercom"]},"websites":{"include":["intercom.com"]}},"page":1}'` | | Hunter | `hunter_email_count` | `deepline tools execute hunter_email_count --payload '{"domain":"stripe.com"}'` | | Hunter | `hunter_discover` | `deepline tools execute hunter_discover --payload '{"query":"B2B SaaS companies","limit":1}'` | | People Data Labs | `peopledatalabs_person_search` | `deepline tools execute peopledatalabs_person_search --payload '{"query":{"bool":{"must":[{"term":{"location_country":"United States"}},{"term":{"job_title_role":"marketing"}}]}},"size":1}'` | | CrustData | `crustdata_v3_person_search` | `deepline tools execute crustdata_v3_person_search --payload '{"filters":{"op":"and","conditions":[{"field":"experience.employment_details.current.company_website_domain","type":"=","value":"notion.so"},{"field":"experience.employment_details.current.title","type":"contains","value":"VP"}]},"limit":1}'` | Notes: Some providers need an actual page pull (small `limit`/`per_page`) instead of dedicated count tools. CrustData `companydb_search`/`persondb_search` don't surface reliable totals -- use for retrieval, not sizing. Always compare `total_count`/`total` with your filter set and stop early when a slice suffices. ### Company-first sourcing ```bash # Size first deepline tools execute dropleads_get_lead_count \ --payload '{ "filters": { "keywords": ["technology"], "employeeRanges": ["51-200"] } }' # Pull list (100 per page) deepline tools execute dropleads_search_people \ --payload '{ "filters": { "keywords": ["technology"], "employeeRanges": ["51-200"] }, "pagination": {"page": 1, "limit": 100} }' ``` ### Contact-first sourcing ```bash deepline tools execute dropleads_search_people \ --payload '{"filters":{"jobTitles":["VP Sales","CRO"],"employeeRanges":["51-200","201-500"],"keywords":["technology"],"personalCountries":{"include":["United States"]}},"pagination":{"page":1,"limit":100}}' ``` ### Signal prioritization Don't outreach the full list. Use `deepline-scoring` skill if you have won/lost data. Otherwise enrich with `crustdata_v3_job_search` (hiring), `exa_search` with `includeDomains` plus `contents` (website/pain language), then score. -
prompts.json 297.9 KB
{ "10-K Analysis of Top Annual Initiatives": [ "\"You are an analyst who is analyzing the strategy of \" + {{input 1: name}} + \". Your job is to summarize the company's top initiatives this year. Specifically focus on the following:\\n\\nTop 5 overall Initiatives\\nTop 3 go to market initiatives\\nTop 3 sales initiatives\\nLaunch of any new products\\nTargeting new segment(s) of customers\\nWhy they are targeting those new segment(s)\\n5 Hypotheses on why the company might be challenged to achieve their top initiatives\\n\\nTo do this: Go to the annual report \" + {{input 2: Link to 10k report}} + \" and analyze the entire document. Take your time and be as thorough as you need to extract the insights.\\n\\nThis is what the output should look like for each section:\\n\\nTop 5 overall Initiatives = List 5, 1 sentence for each\\nTop 3 go to market initiatives = List 3, 1 sentence for each\\nTop 3 sales initiatives = List 3, 1 sentence\\nLaunch of any new products = List products\\nTargeting new segment(s) of customers = List new segments\\nWhy they are targeting those new segment(s) = 2-4 sentences.\\n5 Hypotheses on why the company might be challenged to achieve their top initiatives = list of 5, 1 sentence each\\n\"" ], "5 interesting facts about a candidate": [ "\"Review the provided profile data below, which includes the candidate’s profile.\\n\\nSearch for additional information on this person using their full name (\" + {{input 1: Full Name}} + \") and verify they work or worked at \" + {{input 2: company}} + \"\\n\\n---\\nHere's the profile information: \" + JSON.stringify({{input 3: Enrich Person from Profile}}) + \"\\n---\\n\\nLook for unique insights beyond LinkedIn, such as:\\n- Notable achievements, major projects, or leadership impact\\n- Public recognition (awards, media features, patents, conference talks)\\n- Strategic initiatives they led that had measurable success\\n- Thought leadership (articles, podcasts, interviews)\\n- Any other factors that differentiate them from other candidates\\n\\nSummarize findings into up to 5 key bullet points, ensuring they are concise and high-impact. Output your list like the example below:\\n\\nHighlights:\\n1.\\n2.\\n3.\\n\"" ], "Accelerator participation": [ "\"I need you to determine whether \" + {{input 1: Company}} + \" \" + {{input 2: Domain [Cleaned]}} + \"has participated in any startup accelerator programs. Please follow these steps to ensure accurate results:\\n\\nTask:\\n1. Primary Objective:\\n - Identify and confirm if \" + {{input 1: Company}} + \" has been a part of any startup accelerator programs, such as Y Combinator, Techstars, 500 Startups, MassChallenge, Plug and Play, AngelPad, Seedcamp, Google for Startups Accelerator, DreamIt Ventures, Microsoft Startups Program, Founders Factory, Founder Institute, Alchemist Accelerator, Boost VC, Village Capital, HAX, AlphaLab, Launch, Berkeley SkyDeck, or other recognized Accelerators such as university accelerators.\\n\\n2. Sources to Scrape:\\n - Company Website: Start by checking the company's official website \" + {{input 3: Website [Cleaned, Final]}} + \", particularly sections like \\\"About Us,\\\" \\\"Press,\\\" \\\"News,\\\" or \\\"Our Story\\\" where they might mention participation in an accelerator.\\n - Accelerator Websites: Visit the websites of well-known accelerators (e.g., Y Combinator, Techstars) and search for [\" + {{input 1: Company}} + \"] in their past cohorts or alumni sections.\\n - Press Releases and News Articles: Scrape news sources, press releases, and articles that might announce the company’s acceptance into or graduation from an accelerator program.\\n - Social Media: Check the company's LinkedIn, Twitter, or other social media profiles where they might announce their participation in an accelerator.\\n\\n3. Verification:\\n - Cross-reference the information from multiple reliable sources to confirm whether the company was indeed part of an accelerator. Look for clear indicators such as cohort announcements, accelerator logos on the company’s website, or mentions in reputable news outlets.\\n\\n4. Output:\\n - Return 'Yes' if the company has participated in an accelerator program, or 'No' if no such participation is found. If possible, also include the name of the accelerator program in the output. If the output is \\\"No\\\", return only that and nothing else.\\n\\n5. Accuracy:\\n - Ensure that the result is accurate by verifying against at least two distinct and credible sources. Do not make assumptions based on vague references—confirm explicit participation.\"" ], "AI Outbound - Followup with Event Attendees": [ "\"Write a personalized follow-up email for someone who attended the \"+{{input 1: IN- Event Name}}+\"\\\" event. use info from here as well: \"+{{input 2: Founder Experience}}+\"\\n\\nUse this template structure and fill in the personalized sections based on the prospect information provided:\\n\\nTemplate: Subject: Great connecting at the [insert event name]\\n\\nHey [\"+{{input 3: first_name}}+\"],\\n\\nGreat to see you at the [insert event name]. I took a quick look at [\"+{{input 4: name}}+\"] and noticed you're focused on [their company ICP] - there are actually some really powerful enrichment workflows that work perfectly for that audience when paired with HubSpot sequences.\\nGiven your role as [INSERT: their job title], I imagine you're constantly evaluating which tools actually move the needle vs. create more work. I'd love to show you a few of the workflows we've built that have helped other [INSERT: similar role title] streamline their ABM motion without adding complexity.\\nFeel free to grab 15 minutes here: [Your Calendly link]\\nBest, [Chris] [GTME @ ]\\n\\nRequired inputs to personalize:\\nProspect's name and company: \"+{{input 4: name}}+\" at \"+{{input 4: name}}+\"\\nTheir job title: \"+{{input 6: title}}+\"\\nTheir company's target market/ICP:\"+{{input 7: response}}+\"\\nYour name and title: Chris, GTME @ \\nGenerate the complete personalized email based on the information provided above.\"" ], "Annual Electricity Spend": [ "\"For the company with the following details:\\n\t•\tCompany Name: \"+{{input 1: IN - Account Name}}+\"\\n\t•\tDomain: \"+{{input 2: Final - Website}}+\"\\n\t•\tLinkedIn URL:\"+{{input 3: Final - LinkedIn Company URL}}+\"\\n\\nTask:\\nFind the company’s Annual Electricity Spend (e.g., “$18.8M”).\\nIf you cannot find an exact number or numbers, please output your best guess estimate.\\n\\nSteps to Follow:\\n\t1.\tSearch the company’s official website, including sustainability, financial reports, or energy sections.\\n\t2.\tLook for published Annual Reports, Sustainability Reports, or Energy Use Reports on their website or in PDF format.\\n\t3.\tConduct a Google search using queries like:\\n\t•\t\\\"[Company Name] annual electricity spend site:[Company Domain]\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" electricity costs filetype:pdf\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" sustainability report electricity spend\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" total energy expenses\\\"\\n\t4.\tSearch financial databases, government energy filings, or other industry reports where the company might have disclosed electricity expenses.\\n\t5.\tReview LinkedIn posts or documents shared by the company or its executives for relevant disclosures.\\n\t6.\tLook for press releases or interviews mentioning energy or electricity expenditure.\\n\t7.\tEstimation:\\n\t•\tIf no exact figure is found, derive a reasonable estimate based on:\\n\t•\tCompany size (employee headcount, revenue, etc.).\\n\t•\tIndustry averages for sustainability spend.\\n\t•\tPublicly available data from competitors.\\n\\nOutput Format:\\nReturn only the Annual Electricity Spend figure (e.g., “$18.8M”). If you cannot find an exact number, please provide your best guess estimate as an integer based on your findings, with a tilda in front of the number, e.g., ~$20M. \\nConstraints:\\n\t•\tProvide only the numerical result in USD, unless specified otherwise.\\n\t•\tDo not include any additional commentary or context in the output.\\n\"" ], "Check if a person has attended grad school": [ "\"Scan this LinkedIn, \" + {{input 1: LinkedIn Profile}} + \", to determine if this person ever attended or is attending a graduate school. Only output Yes or No. Output Not found if you cannot find the information\"" ], "Check if a person has the experience needed for a job opening": [ "\"Scrape this LinkedIn, \" + {{input 1: LinkedIn url}} + \", to determine whether or not this person is qualified for this job opening: \" + {{input 2: Url - Jobs}} + \". If they are qualified, output Yes, if not, output No. If you could not find enough data to make a decision, output Undetermined.\"" ], "Check if company gives demos": [ "\"Scrape the pages on this company’s site here, \" + {{input 1: Company Domain}} + \", and determine whether or not a company gives demos. This information would likely be on their pricing page or purchase page. If they have certain pricing plans or options that require you to contact sales and do not just give you the price, that means they likely give demos. If they do give demos, output Yes, if not, output No\"" ], "Check if Company Offers a Free Trial": [ "\"#CONTEXT#\\nVerify if the company associated with \" + {{input 1: Company Domain}} + \" provides a free trial for their product.\\n\\n#OBJECTIVE#\\nDetermine and return whether \" + {{input 1: Company Domain}} + \" offers a free trial.\\n\\n#INSTRUCTIONS#\\n1. Access the official website of the company using the \" + {{input 1: Company Domain}} + \".\\n2. Search for any pages or sections that mention 'Free Trial'. This could be in pricing pages, product pages, or promotional banners.\\n3. Validate the information by ensuring it mentions a specific period or sign-up process for a free trial.\\n4. If there is confirmation of a free trial, return the result as 'Free trial'.\\n5. If there is no specific mention of a free trial, or if it is ambiguous, return 'No Free trial'.\\n6. Do not infer or assume information beyond what is explicitly stated on the company's website.\\n\\n#EXAMPLES#\\nExample Input: Check if company at \" + {{input 1: Company Domain}} + \" has a free trial available.\\nExample Output: 'Free trial' or 'No Free trial'.\\n\"" ], "Check if domain uses Google for MX Record": [ "\"For the domain \" + {{input 1: Generate MX record}} + \", does this contain \\\"google.com\\\" in the text? If yes, return \\\"Yes, uses Google for MX Record\\\". Else, return \\\"No\\\"\"" ], "Classify as B2B or B2C": [ "\"#CONTEXT#\\nYou are tasked with determining the business model of a company based on its online presence and domain.\\n\\n#OBJECTIVE#\\nIdentify if the company associated with the domain \" + {{input 1: Domain}} + \" operates as a B2B, B2C, or both.\\n\\n#INSTRUCTIONS#\\n1. Begin by visiting the company's official website using the provided domain \" + {{input 1: Domain}} + \".\\n2. Review the content on their homepage, services, products, and any available \\\"About Us\\\" or \\\"Company Profile\\\" pages.\\n3. Look for key indicators such as language targeting businesses (B2B) or consumers (B2C). Use terms like \\\"business solutions,\\\" \\\"enterprise,\\\" \\\"clients,\\\" for B2B, and \\\"shop now,\\\" \\\"customer service,\\\" \\\"retail,\\\" for B2C.\\n4. Check any available online press releases, product listings, or service descriptions.\\n5. Search for the target audience mentioned in their marketing materials. Terms like \\\"partners,\\\" \\\"resellers,\\\" signify B2B, while \\\"shoppers,\\\" \\\"individuals,\\\" signify B2C.\\n6. Reference business directories or industry-specific sites if necessary to confirm business model.\\n7. If conflicting information is found, cross-reference with reliable business news or reports.\\n8. Conclude with \\\"B2B,\\\" \\\"B2C,\\\" \\\"B2B and B2C,\\\" or \\\"Cannot determine\\\" based on findings.\\n\\n#EXAMPLES#\\n- For a page focused on enterprise clients and bulk ordering, categorize as \\\"B2B.\\n- For a site featuring retail products with shopping cart options, categorize as \\\"B2C.\\\"\\n- If both audience types are evident, categorize as \\\"B2B and B2C.\\\"\\n- If the information is ambiguous or unclear, result as \\\"Cannot determine.\\\"\"" ], "Classify as B2B or B2C based on company description": [ "\"A B2B company is a company that sells to other companies. A B2C company is a company that sells to consumers. Based on the company’s description, tell me if the company is likely a B2B company or a B2C company. This is the company’s description: \" + {{Description}} + \" The only acceptable output is either B2B or B2C. Do not answer in any other way.\"" ], "Clean job titles": [ "\"I will give you a LinkedIn title that needs to be cleaned, so that it only contains the job title. It is possible that the LinkedIn title includes unnecessary information besides a job title that should be deleted. Shorten the title so it just includes a job title without changing the responsibility of the title. This is the job title I want you to clean: \" + {{Title}}" ], "Company Competitors": [ "\"#CONTEXT#\\nIdentify competitors of a specific company by analyzing its website.\\n\\n#OBJECTIVE#\\nVisit the website of the company at \"+{{input 1: Domain}}+\", understand their business, and list the names of 3 competitors.\\n\\n#INSTRUCTIONS#\\n1. Access the website using the url in \"+{{input 1: Domain}}+\" \\n2. Review the website content to understand what the company does, including their industry and market segment.\\n3. Identify 3 key competitors based on the company's industry and market position\\n4. Return only the names of the competitors, separated by commas, strictly in the format: Competitor 1, Competitor 2, Competitor 3.\\n\\n#EXAMPLES#\\nInput: Visit the website of the company at example.com\\nExpected Output: Competitor A, Competitor B, Competitor C.\"" ], "Company Core Values": [ "\"#CONTEXT#\\nYou are tasked with extracting and determining the core values of a given company based on its publicly available careers and about pages. If they explicitly mention their values, return those. If they do not explicitly mention their values, determine what they likely are.\\n\\nIf the company directly lists phrases they have as values, make sure you just output those. Only generate them if there are not direct ones.\\n\\n#OBJECTIVE#\\nIdentify and return the core values for the company located at \" + {{input 1: Company Domain}} + \".\\n\\n#INSTRUCTIONS#\\n1. Visit the website and find the careers page and about pages. Check for a section that lists \\\"Core Values\\\" or \\\"Company Values.\\\"\\n - If values are listed explicitly, extract them as a comma-separated list.\\n2. If no explicit values are found, go to the \\\"About\\\" page from the \" + {{input 1: Company Domain}} + \".\\n - Look for text indicating the company's mission, vision, or foundational principles.\\n - From there, deduce the core values up to a maximum of 5.\\n3. Ensure that the values extracted or deduced are representative of the company and return them as a concise, comma-separated list.\\n4. If no values can be found or inferred, return \\\"No core values found\\\".\"" ], "Company Customers": [ "\"#CONTEXT# You are tasked with extracting customer names for a specific company from web data based on provided domain information. \\n\\n#OBJECTIVE# Extract and return the names of 3 customers of \"+{{input 1: Domain}}+\" as a comma-separated list. \\n\\n#INSTRUCTIONS# \\n1. Use the \"+{{input 1: Domain}}+\" to locate the company's official website and relevant web pages that list their customers. \\n2. Search for sections like \\\"Our Customers,\\\" \\\"Clients,\\\" or similar headings that often list company clients. \\n3. Extract up to 3 customer names from these sections. \\n4. Ensure that the extracted names are verified as the company's customers through contextual clues or mentions in official sources. \\n5. Return the names as a comma-separated list (e.g., \\\"Customer1, Customer2, Customer3\\\"). \\n\\n#EXAMPLES# \\nExample Input: \\\"example.com\\\" \\nExample Output: \\\"Customer1, Customer2, Customer3\\\"\\n\\n#REMINDERS# \\n- Only retrieve information from legitimate company sources or verified partners. \\n- Do not assume customer names from unrelated or unverifiable sources. Keep the response concise, free of extra commentary. Return \\\"No customers found\\\" if no information about customers is available.\"" ], "Company GTM Strategy Analysis": [ "\"You are an analyst who is analyzing the strategy of \" + {{input 1: Company Domain}} + \". Your job is to summarize the company's top initiatives this year. Specifically focus on the following:\\n\\nTop 5 overall Initiatives\\nTop 3 go to market initiatives\\nTop 3 sales initiatives\\nLaunch of any new products\\nTargeting new segment(s) of customers\\nWhy they are targeting those new segment(s)\\n5 Hypotheses on why the company might be challenged to achieve their top initiatives\\n\\nTo do this: Review the website \" + {{input 1: Company Domain}} + \", look and analyze their 10k if public, find addition news articles.\\n\\nThis is what the output should look like for each section:\\n\\nTop 5 overall Initiatives = List 5, 1 sentence for each\\nTop 3 go to market initiatives = List 3, 1 sentence for each\\nTop 3 sales initiatives = List 3, 1 sentence\\nLaunch of any new products = List products\\nTargeting new segment(s) of customers = List new segments\\nWhy they are targeting those new segment(s) = 2-4 sentences.\\n5 Hypotheses on why the company might be challenged to achieve their top initiatives = list of 5, 1 sentence each\\n\"" ], "Company Mission Statement": [ "\"#CONTEXT# We aim to extract a concise mission statement for a specified company by researching its official domain. \\n\\n#OBJECTIVE# Retrieve a mission statement for the company associated with \"+{{input 1: Domain}}+\" in no more than 3 sentences. \\n\\n#INSTRUCTIONS# 1. Access the homepage or About Us section of the website located at \"+{{input 1: Domain}}+\" . 2. Search for any content that appears to be the company's mission statement. 3. Extract the sentence(s) that best represent the mission or primary business objective of the company. 4. Ensure that the extracted text is concise and within 3 sentences. \\n\\n#EXAMPLES# For example, if the input is Company Domain: \\\"example.com\\\", output may be: \\\"Example Company's mission is to innovate and lead in the tech industry by prioritizing customer satisfaction and sustainable practices.\\\"\"" ], "Company Snapshot": [ "\"You are an analyst who is an expert on marketing and ideal customer profiles (ICP). I'm analyzing \"+{{input 1: Domain}}+\".\\n\\nYour job is to figure out:\\n1. What is \"+{{input 1: Domain}}+\"'s ICP\\n2. What industries does \"+{{input 1: Domain}}+\" target\\n3. What personas does \"+{{input 1: Domain}}+\" target\\n4. What is the primary value proposition of \"+{{input 1: Domain}}+\" for those personas.\\n\\nTo figure this out, analyze the website \"+{{input 1: Domain}}+\" and specifically look at the case studies, who is mentioned in the case studies, who their customers are, blog posts, and general information that positions the problem & solution of the company.\\n\\nTake your time and be as extensive as you need to be. Cost is not an issue and my job depends on bringing back accurate information.\\n\\nReturn back the following:\\nICP: \\\"ICP is ...\\\"\\nTarget Industries: \\\"target 1 and target 2\\\" (Make both plural)\\nTarget personas: \\\"title 1 and title 2\\\" (Make both plural)\\nValue Prop: 1 or 2 sentences\"" ], "Competitive intelligence": [ "\"Answer the questions about the company whose name is \" + {{Company Name}} + \", company description is this \" + {{Description}} + \". Use Breadth-first search to explore all potential scenarios before going deeper into specific ones. Only answer the questions if you have an answer you are 95% sure is correct. If you don't think it's 95% certain, leave it blank. 1. What is the problem this company solves? 2. Without this company, how are people solving this problem today? 3. What is the cost of inaction if people were to stay with their status quo solution instead of switching to this company’s solution? 4. how is this company different from its competitors?\\nKeep each answers under 20 words.\"" ], "Corporate Hierarchy - Initial Structure": [ "\"# CORPORATE TREE RESEARCH PROMPT\\n\\nYou are an expert corporate intelligence analyst specializing in mapping complex business hierarchies and ownership structures. Your expertise encompasses regulatory filings analysis, international corporate law, cross-border ownership structures, and advanced research methodologies across multiple databases and jurisdictions. You excel at finding connections between entities, identifying parent-subsidiary relationships, and constructing complete corporate trees even when dealing with incomplete or imprecise company names. Your research is thorough, methodical, and leverages the most authoritative sources available.\\n\\n## INPUT DEFINITION\\n**COMPANYNAME**: The official business name of the target company (may include legal suffixes like LLC, Inc., Corp, Ltd, etc.)\\n\\n**COMPANYDOMAIN**: The primary website domain of the target company (may include protocol like https:// or www.)\\n\\n**LOCATION**: The location where the business is registered/incorporated\\n\\n**INITIAL_RESEARCH**: Previous research conducted on the company's hierarchical relationship in the following JSON format:\\n```json\\n{\\n \\\"reasoning\\\": \\\"explanation of research methodology and sources used\\\",\\n \\\"confidence\\\": \\\"high|medium|low\\\",\\n \\\"stepsTaken\\\": [\\\"array of specific research steps performed\\\"],\\n \\\"business_description\\\": \\\"detailed company description including location, activities, operational characteristics\\\"\\n}\\n```\\n## INPUT DATA\\n**Company Name**: \"+{{input 1: Company Name}}+\"\\n**Company Domain**: \"+{{input 2: Normalized Company Domain}}+\"\\n**Location**: \"+{{input 3: Company Location}}+\"\\n\\n## EXPERT-LEVEL SEARCH STRATEGY & METHODOLOGY\\nExecute this comprehensive search strategy in priority order. Each step represents what a seasoned corporate intelligence analyst would do:\\n\\n### TIER 1: REGULATORY & LEGAL FILINGS (Highest Authority - Search First)\\n\\n#### 1A. SEC EDGAR Database Deep Dive\\nExecute these EXACT searches on `sec.gov/edgar`:\\n- **Company Search**: `\"+{{input 1: Company Name}}+\"` (try variations: full name, abbreviated, with/without legal suffix)\\n- **Advanced Search Terms**: \\n - `\\\"subsidiary of\\\"` + `\"+{{input 1: Company Name}}+\"`\\n - `\\\"wholly owned by\\\"` + `\"+{{input 1: Company Name}}+\"`\\n - `\\\"parent company\\\"` + `\"+{{input 1: Company Name}}+\"`\\n - `\\\"controlled by\\\"` + `\"+{{input 1: Company Name}}+\"`\\n - `\\\"subsidiaries\\\"` + `\"+{{input 1: Company Name}}+\"`\\n - `\\\"affiliate companies\\\"` + `\"+{{input 1: Company Name}}+\"`\\n- **Specific Filings to Prioritize**:\\n - **10-K Annual Reports**: Look in \\\"Business\\\" section and \\\"Subsidiaries\\\" exhibit\\n - **10-Q Quarterly Reports**: Check for ownership changes\\n - **8-K Current Reports**: Acquisition announcements\\n - **DEF 14A Proxy Statements**: Board composition reveals parent relationships\\n - **Schedule 13D/G**: Beneficial ownership filings\\n - **Form 8-K Item 2.01**: Acquisition completions\\n - **Exhibit 21.1**: Subsidiaries list (most critical for corporate tree)\\n\\n#### 1B. International Regulatory Databases\\n- **Argentina**: `site:cnv.gov.ar` or `site:ign.gob.ar` for \"+{{input 3: Company Location}}+\" companies\\n- **UK Companies House**: `site:companieshouse.gov.uk \"+{{input 1: Company Name}}+\"`\\n- **Canadian SEDAR**: `site:sedar.com \"+{{input 1: Company Name}}+\"`\\n- **Australian ASIC**: `site:asic.gov.au \"+{{input 1: Company Name}}+\"`\\n- **European Company Database**: `site:ec.europa.eu/info/business-economy-euro/company-reporting-and-auditing \"+{{input 1: Company Name}}+\"`\\n\\n#### 1C. Country-Specific Corporate Registries\\nAdapt search based on the company location (\"+{{input 3: Company Location}}+\"):\\n- **Argentina**: `site:afip.gob.ar` (tax registry), provincial commercial registries\\n- **Delaware Division of Corporations**: `site:corp.delaware.gov \"+{{input 1: Company Name}}+\"`\\n- **California Secretary of State**: `site:bizfileonline.sos.ca.gov \"+{{input 1: Company Name}}+\"`\\n- **Nevada Secretary of State**: `site:nvsos.gov COMPANYNAME`\\n\\n### TIER 2: PREMIUM FINANCIAL DATABASES (High Authority)\\n\\n#### 2A. Bloomberg Terminal-Level Searches\\n- **Direct Bloomberg Search**: `site:bloomberg.com/quote \"+{{input 1: Company Name}}+\"`\\n- **Bloomberg Company Profile**: Look specifically for \\\"Company Description\\\" and \\\"Business Summary\\\"\\n- **Corporate Structure**: `site:bloomberg.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"corporate structure\\\" OR \\\"subsidiaries\\\" OR \\\"affiliates\\\")`\\n\\n#### 2B. Reuters Eikon/Refinitiv Searches\\n- **Reuters Company Profile**: `site:reuters.com/companies \"+{{input 1: Company Name}}+\"`\\n- **Ownership Structure**: `site:reuters.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"ownership structure\\\"`\\n- **Corporate Tree**: `site:reuters.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"corporate tree\\\" OR \\\"organizational structure\\\")`\\n\\n#### 2C. S&P Capital IQ Intelligence\\n- **Company Profiles**: `site:capitaliq.com \"+{{input 1: Company Name}}+\"`\\n- **Corporate Structure**: `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"corporate structure\\\" AND (\\\"parent\\\" OR \\\"subsidiary\\\" OR \\\"affiliate\\\")`\\n\\n### TIER 3: VENTURE/PRIVATE EQUITY DATABASES\\n#### 3A. Crunchbase Pro-Level Search\\n- **Direct Company Profile**: `site:crunchbase.com/organization/\"+{{input 1: Company Name}}+\"`\\n- **Advanced Search**: `site:crunchbase.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"acquired by\\\" OR \\\"subsidiary of\\\" OR \\\"corporate structure\\\")`\\n- **Funding History**: `site:crunchbase.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"funding\\\" AND (\\\"series\\\" OR \\\"round\\\")`\\n\\n#### 3B. PitchBook Private Market Intelligence\\n- **Company Database**: `site:pitchbook.com \\\"\"+{{input 1: Company Name}}+\"\\\"`\\n- **Ownership Data**: `site:pitchbook.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"parent company\\\" OR \\\"portfolio company\\\" OR \\\"subsidiaries\\\")`\\n\\n#### 3C. CB Insights\\n- **Company Profile**: `site:cbinsights.com/company/\"+{{input 1: Company Name}}+\"`\\n- **Acquisition Database**: `site:cbinsights.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"acquired\\\"`\\n\\n### TIER 4: COMPANY INTELLIGENCE & DIRECT SOURCES\\n#### 4A. Company Website Forensics\\nVisit `https://\"+{{input 2: Normalized Company Domain}}+\"` and execute these specific searches:\\n**On-Site Search Techniques**:\\n- **Browser Search (Ctrl+F)**: \\\"parent\\\", \\\"subsidiary\\\", \\\"owned by\\\", \\\"part of\\\", \\\"division of\\\", \\\"affiliates\\\", \\\"group companies\\\"\\n- **Site-Specific Google Search**: `site:\"+{{input 2: Normalized Company Domain}}+\" \\\"corporate structure\\\"`\\n- **Key Pages to Examine**:\\n - `/about` or `/about-us` or `/company`\\n - `/investors` or `/investor-relations`\\n - `/news` or `/press-releases`\\n - `/legal` or `/terms`\\n - `/careers` (job descriptions often reveal parent company)\\n - `/corporate-governance`\\n - `/subsidiaries` or `/affiliates`\\n\\n#### 4B. Industry Trade Publications\\n**Technology**: \\n- `site:techcrunch.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"acquired\\\" OR \\\"subsidiary\\\" OR \\\"corporate structure\\\")`\\n- `site:venturebeat.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"parent company\\\"`\\n**Healthcare/Pharma**:\\n- `site:fiercebiotech.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"acquired\\\" OR \\\"owned by\\\")`\\n- `site:biopharmadive.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"parent\\\"`\\n**Financial Services**:\\n- `site:americanbanker.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"subsidiary\\\" OR \\\"owned by\\\")`\\n- `site:investmentnews.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"parent company\\\"`\\n**Retail/E-commerce**:\\n- `site:retaildive.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"acquired\\\" OR \\\"parent\\\")`\\n- `site:digitalcommerce360.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"owned by\\\"`\\n\\n### TIER 5: ADVANCED SEARCH OPERATORS & TECHNIQUES\\n#### 5A. Google Search Mastery\\nExecute these EXACT searches in order:\\n**Basic Ownership Queries**:\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"parent company\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"subsidiary of\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"owned by\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"acquired by\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"division of\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"subsidiaries\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"affiliates\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" \\\"corporate structure\\\"`\\n\\n**Advanced Boolean Searches**:\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"parent company\\\" OR \\\"subsidiary of\\\" OR \\\"owned by\\\" OR \\\"subsidiaries\\\" OR \\\"affiliates\\\") -jobs -careers`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"holding company\\\" OR \\\"controlling interest\\\" OR \\\"corporate tree\\\") -wikipedia`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"corporate structure\\\" OR \\\"organizational chart\\\" OR \\\"group companies\\\")`\\n**Recent Acquisition Searches**:\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"acquired\\\" AND (2020..2025)`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"merger\\\" AND (2020..2025)`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"buyout\\\" AND (2020..2025)`\\n**Financial News Searches**:\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"SEC filing\\\" OR \\\"10-K\\\" OR \\\"10-Q\\\") AND (\\\"parent\\\" OR \\\"subsidiary\\\" OR \\\"affiliates\\\")`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"investor presentation\\\" AND (\\\"corporate structure\\\" OR \\\"ownership\\\")`\\n\\n#### 5B. Specialized Database Searches\\n\\n**Hoovers/D&B**:\\n- `site:hoovers.com \\\"\"+{{input 1: Company Name}}+\"\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"Dun & Bradstreet\\\" AND (\\\"parent company\\\" OR \\\"subsidiaries\\\")`\\n\\n**ZoomInfo**:\\n- `site:zoominfo.com \\\"\"+{{input 1: Company Name}}+\"\\\"`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"ZoomInfo\\\" AND (\\\"parent\\\" OR \\\"subsidiary\\\" OR \\\"corporate structure\\\")`\\n\\n**Factiva/Dow Jones**:\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"corporate structure\\\" AND \\\"Factiva\\\"`\\n\\n#### 5C. International & Cross-Border Searches\\n**For Non-US Companies**:\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"Companies House\\\" OR \\\"ASIC\\\" OR \\\"SEDAR\\\" OR \\\"AFIP\\\")`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"annual report\\\" OR \\\"directors report\\\") AND (\\\"parent\\\" OR \\\"subsidiary\\\" OR \\\"affiliates\\\")`\\n- `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"regulatory filing\\\" OR \\\"disclosure\\\") AND (\\\"ownership\\\" OR \\\"control\\\" OR \\\"corporate structure\\\")`\\n\\n### TIER 6: VERIFICATION & CROSS-REFERENCE\\n\\n#### 6A. News Archive Verification\\n- **Google News Archive**: `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"acquired\\\" OR \\\"merger\\\" OR \\\"subsidiary\\\") AND site:news.google.com`\\n- **Factiva Historical Search**: `\\\"\"+{{input 1: Company Name}}+\"\\\" AND \\\"acquisition\\\" AND (1990..2025)`\\n\\n#### 6B. Legal Document Verification\\n- **Court Records**: `\\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"parent corporation\\\" OR \\\"subsidiary\\\" OR \\\"affiliates\\\") AND \\\"court\\\"`\\n- **Patent Filings**: `site:patents.google.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"assignee\\\" OR \\\"parent\\\")`\\n\\n#### 6C. Academic & Research Sources\\n- **SSRN**: `site:ssrn.com \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"corporate structure\\\" OR \\\"ownership\\\")`\\n- **JSTOR**: `site:jstor.org \\\"\"+{{input 1: Company Name}}+\"\\\" AND (\\\"parent company\\\" OR \\\"subsidiary\\\")`\\n\\n## EDGE CASES\\n1. **Holding Company Structures**: When multiple holding companies exist, distinguish between immediate parent and ultimate/global parent\\n2. **Joint Ventures**: If owned by multiple entities, identify the controlling parent (>50% ownership)\\n3. **Recent Acquisitions**: Verify acquisition completion dates - ignore pending/announced but uncompleted deals\\n4. **Private Equity Ownership**: Treat PE firms as parent companies when they have controlling stakes\\n5. **Spin-offs**: Ensure current ownership status, not historical relationships\\n6. **Shell Companies**: Identify operating parent, not shell holding entities\\n7. **International Structures**: Navigate complex multi-jurisdictional ownership chains\\n8. **Cross-Border Entities**: Map entities across different countries within the same corporate tree\\n\\n## TAX STRUCTURE IDENTIFIER\\nLegal entity types using **exact standardized forms below**:\\n- **ALWAYS use these exact formats** - never variations\\n- Use `null` if no legal structure specified\\n\\n**Tax Structure Standardization Table:**\\n- `\\\"Incorporated\\\"`, `\\\"Inc.\\\"`, `\\\"INC\\\"` → `\\\"Inc\\\"`\\n- `\\\"Corporation\\\"`, `\\\"Corp.\\\"`, `\\\"CORP\\\"` → `\\\"Corp\\\"` \\n- `\\\"Limited\\\"`, `\\\"Ltd.\\\"`, `\\\"LTD\\\"` → `\\\"Ltd\\\"`\\n- `\\\"Company\\\"`, `\\\"Co.\\\"`, `\\\"CO\\\"` → `\\\"Co\\\"`\\n- `\\\"Limited Liability Company\\\"`, `\\\"L.L.C.\\\"`, `\\\"llc\\\"` → `\\\"LLC\\\"`\\n- `\\\"Private Limited\\\"`, `\\\"Pvt. Ltd.\\\"`, `\\\"PVT LTD\\\"` → `\\\"Pvt Ltd\\\"`\\n- `\\\"Public Limited Company\\\"`, `\\\"P.L.C.\\\"` → `\\\"PLC\\\"`\\n- `\\\"Professional Association\\\"`, `\\\"P.A.\\\"`, `\\\"PA\\\"` → `\\\"P.A.\\\"`\\n- `\\\"Limited Partnership\\\"`, `\\\"L.P.\\\"`, `\\\"LP\\\"` → `\\\"L.P.\\\"`\\n- `\\\"Proprietary Limited\\\"`, `\\\"Pty. Ltd.\\\"`, `\\\"PTY LTD\\\"` → `\\\"Pty Ltd\\\"`\\n- `\\\"Private Limited\\\"`, `\\\"Pte. Ltd.\\\"`, `\\\"PTE LTD\\\"` → `\\\"Pte Ltd\\\"`\\n- `\\\"Limitada\\\"`, `\\\"LTDA\\\"`, `\\\"Ltda.\\\"` → `\\\"Ltda\\\"`\\n- `\\\"Sociedad Anonima\\\"`, `\\\"S.A.\\\"`, `\\\"SA\\\"` → `\\\"S.A.\\\"`\\n- `\\\"Sociedad Anonima de Capital Variable\\\"`, `\\\"S.A. de C.V.\\\"` → `\\\"S.A. de C.V.\\\"`\\n- `\\\"Gesellschaft mit beschränkter Haftung\\\"`, `\\\"G.m.b.H.\\\"`, `\\\"gmbh\\\"` → `\\\"GmbH\\\"`\\n- `\\\"Aktiengesellschaft\\\"`, `\\\"A.G.\\\"`, `\\\"ag\\\"` → `\\\"AG\\\"`\\n- `\\\"Aktiebolag\\\"`, `\\\"A.B.\\\"`, `\\\"ab\\\"` → `\\\"AB\\\"`\\n- `\\\"Perseroan Terbatas\\\"`, `\\\"P.T.\\\"`, `\\\"pt\\\"` → `\\\"PT\\\"`\\n\\n## DATA HANDLING POLICIES\\n\\n### CONFLICTING DATA POLICY\\nWhen sources provide conflicting information:\\n1. Prioritize SEC filings over all other sources\\n2. Use most recent official company communications\\n3. Cross-reference with at least 2 additional authoritative sources\\n4. When in doubt, use the most conservative/direct parent relationship\\n\\n### INCONCLUSIVE DATA POLICY\\nWhen data is unclear or incomplete:\\n1. Search for alternative company names/DBAs\\n2. Check for recent corporate restructuring announcements\\n3. Verify company is still operational (not defunct/dissolved)\\n4. If still inconclusive after exhaustive search, output empty string\\n\\n### NULL RESULTS POLICY\\nOutput empty string (\\\"\\\") for company fields when:\\n- No parent/subsidiary companies exist (truly independent company)\\n- Cannot verify relationships with confidence after exhaustive search\\n- Company appears to be defunct or non-operational\\n\\n## OUTPUT FORMAT\\nReturn results in exactly this JSON structure:\\n```json\\n{\\n \\\"domestic_entities\\\": [\\n {\\n \\\"entity_name\\\": \\\"EXACT_LEGAL_NAME\\\",\\n \\\"entity_domain\\\": \\\"DOMAIN_OR_EMPTY_STRING\\\",\\n \\\"tax_identifier\\\": \\\"STANDARDIZED_LEGAL_STRUCTURE_OR_NULL\\\",\\n \\\"immediate_parent\\\": \\\"IMMEDIATE_PARENT_ENTITY_NAME_OR_EMPTY_STRING\\\",\\n \\\"notes\\\": \\\"ADDITIONAL_CONTEXT_OR_EMPTY_STRING\\\"\\n }\\n ],\\n \\\"regional_parents\\\": [\\n {\\n \\\"entity_name\\\": \\\"EXACT_LEGAL_NAME\\\",\\n \\\"entity_domain\\\": \\\"DOMAIN_OR_EMPTY_STRING\\\",\\n \\\"tax_identifier\\\": \\\"STANDARDIZED_LEGAL_STRUCTURE_OR_NULL\\\",\\n \\\"country\\\": \\\"COUNTRY_OF_INCORPORATION\\\",\\n \\\"immediate_parent\\\": \\\"IMMEDIATE_PARENT_ENTITY_NAME_OR_EMPTY_STRING\\\",\\n \\\"notes\\\": \\\"ADDITIONAL_CONTEXT_OR_EMPTY_STRING\\\"\\n }\\n ],\\n \\\"global_parent\\\": {\\n \\\"entity_name\\\": \\\"EXACT_LEGAL_NAME_OR_EMPTY_STRING\\\",\\n \\\"entity_domain\\\": \\\"DOMAIN_OR_EMPTY_STRING\\\",\\n \\\"tax_identifier\\\": \\\"STANDARDIZED_LEGAL_STRUCTURE_OR_NULL\\\",\\n \\\"country\\\": \\\"COUNTRY_OF_INCORPORATION\\\",\\n \\\"immediate_parent\\\": \\\"EMPTY_STRING_IF_ULTIMATE_PARENT\\\",\\n \\\"notes\\\": \\\"ADDITIONAL_CONTEXT_OR_EMPTY_STRING\\\"\\n },\\n \\\"research_methodology\\\": {\\n \\\"sources_consulted\\\": [\\\"LIST_OF_PRIMARY_SOURCES\\\"],\\n \\\"confidence_level\\\": \\\"high|medium|low\\\",\\n \\\"limitations\\\": \\\"ANY_RESEARCH_LIMITATIONS_OR_GAPS\\\"\\n }\\n}\\n```\\n### OUTPUT FORMATTING RULES\\n- **Primary Focus**: Map ALL potential entities within the input country that could match the input company name\\n- **Immediate Parent Chain**: Each entity's immediate_parent field enables construction of the full corporate hierarchy\\n- **Regional Parents**: Include intermediate regional/continental parents before the global parent\\n- **Global Parent**: Ultimate parent company (immediate_parent will be empty string)\\n- **Domain Format**: Use bare domain format (example.com, not https://www.example.com)\\n- **Empty Values**: Use empty string \\\"\\\" for immediate_parent if no parent exists\\n- **Entity Verification**: Include all potential matches since input names may not be exact\\n- **Tax Identifier**: Must use exact standardized formats from the table above\\n\\n## EXAMPLES\\n\\n### EXAMPLE 1: DOMESTIC CORPORATE STRUCTURE WITH FOREIGN PARENT\\n**Input Data:**\\n- Company Name: LUSQTOFF ARGENTINA S.A.\\n- Company Domain: lusqtoff.com.ar\\n- Country: Argentina\\n**Output:**\\n```json\\n{\\n \\\"domestic_entities\\\": [\\n {\\n \\\"entity_name\\\": \\\"LUSQTOFF ARGENTINA S.A.\\\",\\n \\\"entity_domain\\\": \\\"lusqtoff.com.ar\\\",\\n \\\"tax_identifier\\\": \\\"S.A.\\\",\\n \\\"relationship_to_target\\\": \\\"target_company\\\",\\n \\\"notes\\\": \\\"Target company - tools and equipment distributor based in Paso del Rey, Buenos Aires\\\"\\n },\\n {\\n \\\"entity_name\\\": \\\"LUSQTOFF SERVICIOS S.R.L.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"tax_identifier\\\": \\\"S.R.L.\\\",\\n \\\"relationship_to_target\\\": \\\"immediate_subsidiary\\\",\\n \\\"notes\\\": \\\"Service subsidiary handling technical support and training\\\"\\n },\\n {\\n \\\"entity_name\\\": \\\"LUSQTOFF DISTRIBUCIONES S.A.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"tax_identifier\\\": \\\"S.A.\\\",\\n \\\"relationship_to_target\\\": \\\"affiliate\\\",\\n \\\"notes\\\": \\\"Regional distribution affiliate for northern Argentina\\\"\\n }\\n ],\\n \\\"foreign_parent\\\": {\\n \\\"entity_name\\\": \\\"LUSQTOFF HOLDINGS CORP\\\",\\n \\\"entity_domain\\\": \\\"lusqtoff.com\\\",\\n \\\"tax_identifier\\\": \\\"Corp\\\",\\n \\\"country\\\": \\\"United States\\\",\\n \\\"notes\\\": \\\"Ultimate parent company registered in Delaware, controls Latin American operations\\\"\\n },\\n \\\"research_methodology\\\": {\\n \\\"sources_consulted\\\": [\\\"Argentina Commercial Registry\\\", \\\"AFIP Tax Database\\\", \\\"Company Website\\\", \\\"SEC EDGAR\\\"],\\n \\\"confidence_level\\\": \\\"high\\\",\\n \\\"limitations\\\": \\\"Some affiliate relationships estimated from partial disclosures\\\"\\n }\\n}\\n```\\n### EXAMPLE 2: PURELY DOMESTIC CORPORATE GROUP\\n**Input Data:**\\n- Company Name: Grupo Familiar Empresarial S.A.\\n- Company Domain: grupofamiliar.com.ar\\n- Country: Argentina\\n**Output:**\\n```json\\n{\\n \\\"domestic_entities\\\": [\\n {\\n \\\"entity_name\\\": \\\"Holding Familiar S.A.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"tax_identifier\\\": \\\"S.A.\\\",\\n \\\"relationship_to_target\\\": \\\"immediate_parent\\\",\\n \\\"notes\\\": \\\"Family holding company controlling multiple subsidiaries\\\"\\n },\\n {\\n \\\"entity_name\\\": \\\"Grupo Familiar Empresarial S.A.\\\",\\n \\\"entity_domain\\\": \\\"grupofamiliar.com.ar\\\",\\n \\\"tax_identifier\\\": \\\"S.A.\\\",\\n \\\"relationship_to_target\\\": \\\"target_company\\\",\\n \\\"notes\\\": \\\"Main operating company\\\"\\n },\\n {\\n \\\"entity_name\\\": \\\"Familiar Logística S.R.L.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"tax_identifier\\\": \\\"S.R.L.\\\",\\n \\\"relationship_to_target\\\": \\\"immediate_subsidiary\\\",\\n \\\"notes\\\": \\\"Logistics and distribution subsidiary\\\"\\n },\\n {\\n \\\"entity_name\\\": \\\"Familiar Servicios S.R.L.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"tax_identifier\\\": \\\"S.R.L.\\\",\\n \\\"relationship_to_target\\\": \\\"immediate_subsidiary\\\",\\n \\\"notes\\\": \\\"Professional services subsidiary\\\"\\n }\\n ],\\n \\\"foreign_parent\\\": {\\n \\\"entity_name\\\": \\\"\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"tax_identifier\\\": null,\\n \\\"country\\\": \\\"\\\",\\n \\\"notes\\\": \\\"\\\"\\n },\\n \\\"research_methodology\\\": {\\n \\\"sources_consulted\\\": [\\\"Argentina Commercial Registry\\\", \\\"AFIP Database\\\", \\\"Company Website\\\"],\\n \\\"confidence_level\\\": \\\"high\\\",\\n \\\"limitations\\\": \\\"\\\"\\n }\\n}\\n```\\n### EXAMPLE 3: INDEPENDENT SINGLE ENTITY\\n**Input Data:**\\n- Company Name: Empresa Local Independiente S.R.L.\\n- Company Domain: localindependiente.com.ar\\n- Country: Argentina\\n**Output:**\\n```json\\n{\\n \\\"domestic_entities\\\": [\\n {\\n \\\"entity_name\\\": \\\"Empresa Local Independiente S.R.L.\\\",\\n \\\"entity_domain\\\": \\\"localindependiente.com.ar\\\",\\n \\\"tax_identifier\\\": \\\"S.R.L.\\\",\\n \\\"relationship_to_target\\\": \\\"target_company\\\",\\n \\\"notes\\\": \\\"Independent company with no parent or subsidiary structure\\\"\\n }\\n ],\\n \\\"foreign_parent\\\": {\\n \\\"entity_name\\\": \\\"\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"tax_identifier\\\": null,\\n \\\"country\\\": \\\"\\\",\\n \\\"notes\\\": \\\"\\\"\\n },\\n \\\"research_methodology\\\": {\\n \\\"sources_consulted\\\": [\\\"Argentina Commercial Registry\\\", \\\"AFIP Database\\\", \\\"Company Website\\\"],\\n \\\"confidence_level\\\": \\\"high\\\",\\n \\\"limitations\\\": \\\"\\\"\\n }\\n}\\n```\\n## EXECUTION INSTRUCTIONS\\n1. **Comprehensive Entity Discovery**: Map ALL potential entities within the input country that could match or relate to the input company name (which may be unclean/imprecise)\\n2. **Immediate Parent Chain**: Structure each entity with its immediate_parent to enable full corporate tree construction\\n3. **Regional Parent Recognition**: Identify intermediate regional/continental parents before reaching the global parent\\n4. **Multiple Potential Matches**: Include all entities that could potentially match the input name since names may not be exact\\n5. **Research Priority**: Begin with TIER 1 country-specific sources before moving to international sources\\n6. **Leverage Initial Research**: Use provided initial research to focus search efforts and validate findings\\n7. **Cross-Reference**: Verify findings across multiple authoritative sources within the target country\\n8. **Current Information**: Ensure all entity information is current (not historical)\\n9. **Tax Identifier Standardization**: Apply standardization consistently for entity matching\\n10. **Corporate Tree Construction**: Enable downstream processing to build complete hierarchical structures using immediate_parent relationships\\n11. **Entity Resolution Focus**: Structure output to facilitate matching unclean input data to correct entities within the domestic corporate landscape\"" ], "Corporate Hierarchy JSON": [ "\"# Corporate Hierarchy Analysis & Structuring Prompt\\nYou are an expert corporate structure analyst tasked with analyzing and synthesizing multiple pre-collected data sources to determine the definitive corporate hierarchy for a given company. All data sources have already been gathered - your role is to analyze, cross-reference, prioritize, and resolve conflicts between these existing data sources to produce a structured corporate hierarchy in JSON format.\\n\\n## Input Definition\\n\\n### Primary Input Data\\n- **input_company_name**: The exact legal name of the target company being analyzed\\n- **company_domain**: The primary domain/website of the target company\\n- **input_company_address**: The confirmed address of the target company (already scraped and verified)\\n\\n### Pre-Collected Data Sources (Already Gathered)\\n1. **hh_emea_hierarchy_data**: HitHorizons EMEA company data containing:\\n - results: Array of company entities with fields including CompanyName, Country, City, NationalId, TaxId, Websites, EmailDomains, HitHorizonsId, CompanyType, LocationType, etc.\\n2. **hg_insights_hierarchy_data**: HG Insights company hierarchy containing:\\n - entities: Array with company_name, hg_company_id, company_domain, hierarchy_tiers, is_group_hq, is_domestic_parent, is_corporate_parent, is_lower_level_entity flags\\n3. **corporate_hierarchy_from_web**: Structured hierarchy from web research containing:\\n - reasoning: Detailed analysis methodology\\n - confidence: Data quality assessment\\n - stepsTaken: Research sources\\n - global_parent: Ultimate parent entity details\\n - regional_parents: Regional holding companies\\n - domestic_entities: Domestic subsidiaries\\n - research_methodology: Limitations and confidence details\\n\\n## Input Data\\n```\\ninput_company_name: \"+{{input 1: Company Name}}+\"\\ncompany_domain: \"+{{input 2: Normalized Company Domain}}+\"\\ninput_company_address: \"+{{input 3: Company Location}}+\"\\nhh_emea_hierarchy_data: \"+{{input 4: Find EMEA Company Firmographics}}+\"\\nhg_insights_hierarchy_data: \"+{{input 5: Find company corporate structure}}+\"\\ncorporate_hierarchy_from_web: \"+{{input 6: Corporate Hierarchy - Initial Structure}}+\"\\n```\\n\\n## Corporate Hierarchy Understanding\\n\\n### Legal Entity Focus\\n**CRITICAL**: Only include actual legal entities in corporate hierarchy output. Exclude:\\n- Regional offices, branch offices, or sales offices (unless separately incorporated)\\n- Operating divisions or business units (unless separate legal entities) \\n- Individual practitioners working under corporate umbrellas\\n- Non-incorporated subsidiaries or departments\\n\\n### Legal Entity Identification Criteria\\n**Include only entities that meet ALL criteria:**\\n1. **Separate Legal Incorporation**: Entity has distinct legal existence (Corp, LLC, Ltd, plc, GmbH, EOOD, etc.)\\n2. **Independent Legal Liability**: Entity can be held legally liable independent of parent\\n3. **Regulatory Filing Status**: Entity files separate regulatory/tax documents (has EIN, tax ID, or equivalent)\\n4. **Contractual Capacity**: Entity can enter contracts in its own name\\n\\n### Legal Entity Types to Recognize\\n- **Standard Corporate Entities**: Traditional corporations, LLCs with standard structures\\n- **European/International Entities**: AG, GmbH, SA, AB, AS, BV, NV, Ltd, PLC, EOOD, SRL with jurisdiction-specific legal frameworks\\n- **Professional Services Partnerships**: LLP structures in legal/accounting with partner ownership\\n- **Franchise Entities**: Separate franchisee legal entities operating under brand licensing agreements\\n\\n### Hierarchy Alignment Rules\\n- **Regional offices** → Align with nearest parent legal entity\\n- **Sales divisions** → Align with parent corporation\\n- **Franchise locations** → Identify actual franchisee legal entity, not brand name\\n\\n## Entity Deduplication Logic\\n\\n### Primary Deduplication Criteria\\n**CRITICAL**: Before positioning input company, check for entity matches using:\\n1. **Legal Name Normalization**:\\n - Remove punctuation differences: \\\"Inc.\\\" vs \\\"Inc\\\" vs \\\"Incorporated\\\"\\n - Standardize spacing and capitalization\\n - Handle common abbreviations: \\\"Company\\\" vs \\\"Co.\\\", \\\"Corporation\\\" vs \\\"Corp\\\"\\n2. **Tax ID/Registration Number Matching**:\\n - Match on NationalId (from HitHorizons), tax_identifier, EIN, or other registration numbers\\n - Priority: Tax ID match overrides name variations\\n3. **Domain + Address Matching**:\\n - Same domain (from Websites array or company_domain) + same headquarters = likely same entity\\n - Cross-reference with incorporation jurisdiction\\n4. **Legal Entity Suffix Analysis**:\\n - Different suffixes may indicate different entities: \\\"ABC Corp\\\" vs \\\"ABC LLC\\\"\\n - European suffixes (GmbH, BV, Ltd) require jurisdiction verification\\n\\n### Deduplication Process\\n1. **Extract all entities** from all data sources\\n2. **Normalize legal names** using standardization rules\\n3. **Match on identifiers**: NationalId, TaxId, HitHorizonsId, hg_company_id\\n4. **Group by normalized names** and check domains, addresses\\n5. **Merge duplicate entities** preserving most complete data\\n6. **Rebuild hierarchy levels** ensuring no duplicate entities exist\\n\\n## Analysis Methodology\\n\\n### Step 1: Parse Corporate Structure Summary\\n- Extract key insights from general_company_hierarchy_scrape\\n- Identify parent company names, subsidiary relationships, special structures\\n- Note any dual-class shares, partnership models, or unique considerations\\n\\n### Step 2: Extract & Deduplicate All Entities\\n- Start with corporate_hierarchy_from_web structure as base\\n- Add entities from hg_insights_hierarchy_data using hierarchy_tiers\\n- Incorporate hh_emea_hierarchy_data entities (especially for European subsidiaries)\\n- Apply deduplication logic across all sources\\n\\n### Step 3: Validate Legal Entity Status\\n- Confirm each entity meets legal entity criteria\\n- Use CompanyType and LocationType from HitHorizons for validation\\n- Cross-reference with tax_identifier and legal suffixes\\n\\n### Step 4: Build Hierarchy Levels\\n- Use is_group_hq flag to identify ultimate parent entities\\n- Apply hierarchy_tiers to determine relative positions\\n- Leverage immediate_parent relationships from structured data\\n- Position entities based on is_domestic_parent and is_corporate_parent flags\\n\\n### Step 5: Identify Input Company's Position\\n- Match input company against deduplicated entity list\\n- Use input_company_address for validation\\n- Determine its position in the hierarchy\\n\\n### Step 6: Extract Immediate Subsidiaries\\n**CRITICAL**: Only include entities that are DIRECT subsidiaries of the input company\\n- Check immediate_parent field equals input company name\\n- Review relationship_to_target for \\\"immediate_subsidiary\\\" designation\\n- Exclude grandchild entities (subsidiaries of subsidiaries)\\n- Validate each subsidiary meets legal entity criteria\\n\\n### Step 7: Enrich with Addresses and Domains\\n- Pull headquarters_address from corporate_hierarchy_with_addresses\\n- Extract domains from Websites arrays and company_domain fields\\n- Use HitHorizons address fields for EMEA entities\\n- Ensure each entity has associated domain where available\\n\\n### Step 8: Resolve Conflicts & Assign Confidence\\n- Prioritize structured hierarchy data over text summaries\\n- Assess overall data quality and consistency\\n- Document key decisions in reasoning field\\n\\n## Source Prioritization Rules\\n1. **For Legal Names**: corporate_hierarchy_from_web > HitHorizons CompanyName > hg_insights company_name\\n2. **For Tax IDs**: HitHorizons NationalId/TaxId > corporate_hierarchy_from_web tax_identifier\\n3. **For Hierarchy Position**: hg_insights hierarchy_tiers and flags > corporate_hierarchy_from_web structure\\n4. **For Addresses**: corporate_hierarchy_with_addresses > HitHorizons address fields\\n5. **For Domains**: Direct company_domain/entity_domain > Websites array > EmailDomains parsing\\n6. **For Subsidiaries**: corporate_hierarchy_from_web immediate_parent relationships > hg_insights lower_level_entity flags\\n\\n## Edge Cases & Handling Policies\\n\\n### Edge Case 1: Circular References\\n**Scenario**: Entity A owns Entity B which owns Entity A\\n**Policy**: Flag as data error; exclude circular relationships from output\\n\\n### Edge Case 2: Joint Ventures\\n**Scenario**: Entity owned equally by multiple parents\\n**Policy**: Include all parent companies in parent_hierarchy with notes in reasoning\\n\\n### Edge Case 3: Sister Companies\\n**Scenario**: Companies share same parent but no direct relationship\\n**Policy**: Exclude from subsidiary_hierarchy; only include if they share immediate parent with input company\\n\\n### Edge Case 4: Indirect Subsidiaries\\n**Scenario**: Subsidiary of a subsidiary appears in data\\n**Policy**: Strictly exclude unless data shows direct immediate_parent relationship to input company\\n\\n### Edge Case 5: Holding vs Operating Companies\\n**Scenario**: Multiple entities with similar names, some holding companies\\n**Policy**: Include all legal entities; clarify holding vs operating status in reasoning\\n\\n## Output Format\\n\\n### Required JSON Structure\\n```json\\n{\\n \\\"input_company\\\": [\\n {\\n \\\"hq_territory\\\": \\\"CITY_STATE_COUNTRY or COUNTRY_MINIMUM\\\",\\n \\\"legal_name\\\": \\\"FULL_LEGAL_NAME_WITH_TAX_ID_IF_AVAILABLE\\\",\\n \\\"company_domain\\\": \\\"domain.com\\\"\\n }\\n ],\\n \\\"parent_hierarchy\\\": [\\n {\\n \\\"level\\\": \\\"L1\\\",\\n \\\"hq_territory\\\": \\\"CITY_STATE_COUNTRY or COUNTRY_MINIMUM\\\",\\n \\\"legal_name\\\": \\\"FULL_LEGAL_NAME_WITH_TAX_ID_IF_AVAILABLE\\\",\\n \\\"company_domain\\\": \\\"domain.com\\\"\\n },\\n {\\n \\\"level\\\": \\\"L2\\\",\\n \\\"hq_territory\\\": \\\"CITY_STATE_COUNTRY or COUNTRY_MINIMUM\\\",\\n \\\"legal_name\\\": \\\"FULL_LEGAL_NAME_WITH_TAX_ID_IF_AVAILABLE\\\",\\n \\\"company_domain\\\": \\\"domain.com\\\"\\n }\\n ],\\n \\\"subsidiary_hierarchy\\\": [\\n {\\n \\\"hq_territory\\\": \\\"CITY_STATE_COUNTRY or COUNTRY_MINIMUM\\\",\\n \\\"legal_name\\\": \\\"FULL_LEGAL_NAME_WITH_TAX_ID_IF_AVAILABLE\\\",\\n \\\"company_domain\\\": \\\"domain.com\\\"\\n }\\n ],\\n \\\"confidence_level\\\": \\\"high|medium|low\\\",\\n \\\"reasoning\\\": \\\"BRIEF_EXPLANATION_OF_ANALYSIS_AND_KEY_DECISIONS\\\"\\n}\\n```\\n### Output Structure Notes\\n- **input_company**: Always contains exactly one entity - the target company being analyzed\\n- **parent_hierarchy**: Ordered from ultimate parent (L1) down to immediate parent\\n - L1 = Ultimate/Global parent company\\n - L2 = Regional or intermediate parent\\n - L3+ = Additional hierarchy levels leading to input company\\n- **subsidiary_hierarchy**: ONLY immediate/direct subsidiaries of input company\\n - Do NOT include subsidiaries of subsidiaries\\n - Do NOT include sister companies or affiliates\\n - Each entity must have input company as its immediate_parent\\n\\n## Examples\\n\\n### Example 1: Multi-Level Hierarchy with Subsidiaries\\n**Input Data:**\\n```\\ninput_company_name: Atlassian Holdings B.V.\\ncompany_domain: atlassian.com\\ninput_company_address: Singel 236, Amsterdam, 1016 AB, Netherlands\\ncorporate_hierarchy_from_web: {\\n \\\"global_parent\\\": {\\n \\\"entity_name\\\": \\\"Atlassian Corporation Plc\\\",\\n \\\"entity_domain\\\": \\\"atlassian.com\\\",\\n \\\"country\\\": \\\"United Kingdom\\\"\\n },\\n \\\"regional_parents\\\": [\\n {\\n \\\"entity_name\\\": \\\"Atlassian Holdings B.V.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"country\\\": \\\"Netherlands\\\",\\n \\\"immediate_parent\\\": \\\"Atlassian Corporation Plc\\\"\\n }\\n ],\\n \\\"domestic_entities\\\": [\\n {\\n \\\"entity_name\\\": \\\"Atlassian B.V.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"immediate_parent\\\": \\\"Atlassian Holdings B.V.\\\"\\n },\\n {\\n \\\"entity_name\\\": \\\"Atlassian International B.V.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"immediate_parent\\\": \\\"Atlassian Holdings B.V.\\\"\\n },\\n {\\n \\\"entity_name\\\": \\\"Atlassian Europe B.V.\\\",\\n \\\"entity_domain\\\": \\\"\\\",\\n \\\"immediate_parent\\\": \\\"Atlassian International B.V.\\\"\\n }\\n ]\\n}\\n```\\n**Output:**\\n```json\\n{\\n \\\"input_company\\\": [\\n {\\n \\\"hq_territory\\\": \\\"Amsterdam, Noord-Holland, Netherlands\\\",\\n \\\"legal_name\\\": \\\"Atlassian Holdings B.V.\\\",\\n \\\"company_domain\\\": \\\"atlassian.com\\\"\\n }\\n ],\\n \\\"parent_hierarchy\\\": [\\n {\\n \\\"level\\\": \\\"L1\\\",\\n \\\"hq_territory\\\": \\\"London, United Kingdom\\\",\\n \\\"legal_name\\\": \\\"Atlassian Corporation Plc\\\",\\n \\\"company_domain\\\": \\\"atlassian.com\\\"\\n }\\n ],\\n \\\"subsidiary_hierarchy\\\": [\\n {\\n \\\"hq_territory\\\": \\\"Amsterdam, Noord-Holland, Netherlands\\\",\\n \\\"legal_name\\\": \\\"Atlassian B.V.\\\",\\n \\\"company_domain\\\": \\\"atlassian.com\\\"\\n },\\n {\\n \\\"hq_territory\\\": \\\"Amsterdam, Noord-Holland, Netherlands\\\",\\n \\\"legal_name\\\": \\\"Atlassian International B.V.\\\",\\n \\\"company_domain\\\": \\\"atlassian.com\\\"\\n }\\n ],\\n \\\"confidence_level\\\": \\\"high\\\",\\n \\\"reasoning\\\": \\\"Clear hierarchy established with Atlassian Holdings B.V. as regional parent under global parent Atlassian Corporation Plc. Two immediate subsidiaries identified: Atlassian B.V. and Atlassian International B.V. Note: Atlassian Europe B.V. excluded as it's a subsidiary of Atlassian International B.V., not the input company.\\\"\\n}\\n```\\n### Example 2: Subsidiary Company with No Children\\n**Input Data:**\\n```\\ninput_company_name: Loom Inc.\\ncompany_domain: loom.com\\ninput_company_address: 140 2nd St, Fl 3, San Francisco, CA 94105\\n[Data showing Loom as subsidiary of Atlassian with no subsidiaries of its own]\\n```\\n**Output:**\\n```json\\n{\\n \\\"input_company\\\": [\\n {\\n \\\"hq_territory\\\": \\\"San Francisco, California, United States\\\",\\n \\\"legal_name\\\": \\\"Loom Inc.\\\",\\n \\\"company_domain\\\": \\\"loom.com\\\"\\n }\\n ],\\n \\\"parent_hierarchy\\\": [\\n {\\n \\\"level\\\": \\\"L1\\\",\\n \\\"hq_territory\\\": \\\"Sydney, Australia\\\",\\n \\\"legal_name\\\": \\\"Atlassian Corporation Plc\\\",\\n \\\"company_domain\\\": \\\"atlassian.com\\\"\\n }\\n ],\\n \\\"subsidiary_hierarchy\\\": [],\\n \\\"confidence_level\\\": \\\"high\\\",\\n \\\"reasoning\\\": \\\"Loom Inc. identified as direct subsidiary of Atlassian Corporation Plc following November 2023 acquisition. No subsidiaries found for Loom Inc.\\\"\\n}\\n```\\n### Example 3: Ultimate Parent with Multiple Subsidiaries\\n**Input Data:**\\n```\\ninput_company_name: Microsoft Corporation\\ncompany_domain: microsoft.com\\n[Data showing Microsoft as ultimate parent with various subsidiaries]\\n```\\n**Output:**\\n```json\\n{\\n \\\"input_company\\\": [\\n {\\n \\\"hq_territory\\\": \\\"Redmond, Washington, United States\\\",\\n \\\"legal_name\\\": \\\"Microsoft Corporation\\\",\\n \\\" -
skill-metadata.json 3.1 KB
{ "title": "GTM Meta Skill", "documents": { "SKILL.md": { "kind": "entrypoint", "title": "GTM Meta Skill", "tags": ["gtm", "router"], "providers": [] }, "finding-companies-and-contacts.md": { "kind": "guide", "title": "Finding Companies and Contacts", "tags": ["prospecting", "search", "discovery"], "providers": [] }, "enriching-and-researching.md": { "kind": "guide", "title": "Enriching and Researching", "tags": ["enrichment", "waterfall", "signals"], "providers": [] }, "writing-outreach.md": { "kind": "guide", "title": "Writing Outreach", "tags": ["qualification", "email", "outreach"], "providers": [] }, "agents/execution-plan-creator.md": { "kind": "agent", "title": "Execution Plan Creator", "tags": ["agent", "planning"], "providers": [] }, "agents/list-builder.md": { "kind": "agent", "title": "List Builder", "tags": ["agent", "prospecting"], "providers": [] }, "references/clay-action-mappings.md": { "kind": "reference", "title": "Clay Action Mappings", "tags": ["reference", "clay"], "providers": [] }, "references/clay-api-surface.md": { "kind": "reference", "title": "Clay Internal API Surface", "tags": ["reference", "clay"], "providers": [] }, "references/clay-extraction.md": { "kind": "reference", "title": "Clay Extraction", "tags": ["reference", "clay"], "providers": [] }, "references/contact-accuracy.md": { "kind": "reference", "title": "Contact Accuracy", "tags": ["reference", "contacts"], "providers": [] }, "references/plays-api-reference.md": { "kind": "reference", "title": "Plays API Reference", "tags": ["reference", "plays"], "providers": [] }, "references/plays-run-export-inspect-repair.md": { "kind": "reference", "title": "Plays Run Export Inspect Repair", "tags": ["reference", "plays"], "providers": [] }, "references/plays-sdk-reference.md": { "kind": "reference", "title": "Plays SDK Reference", "tags": ["reference", "plays"], "providers": [] }, "references/monitor-contract-reference.md": { "kind": "reference", "title": "Monitor Contract Reference", "tags": ["reference", "monitors"], "providers": [] } }, "prefixes": { "agents/": { "kind": "agent", "title": "Agent", "tags": ["agent"], "providers": [] }, "provider-playbooks/": { "kind": "provider-playbook", "title": "Provider Playbook", "tags": ["provider"], "providers": [] }, "references/": { "kind": "reference", "title": "Reference", "tags": ["reference"], "providers": [] }, "recipes/": { "kind": "specialized-workflow", "title": "Recipe", "tags": ["recipe"], "providers": [] } }, "default_document": { "kind": "other", "title": "Document", "tags": [], "providers": [] } } -
SKILL.md 42 KB
--- name: deepline-gtm description: "GTM prospecting, enrichment, outreach, and Deepline Play work/audits. Providers: adyntel,affinity,ai_ark,akta,allegrow,amplemarket,apify,attention,attio,aviato,bettercontact,bigquery,bluesky,bounceban,brave,browserbase,builtwith,clay,clickhouse,cloudflare,contactout,contextdev,crustdata-v3,customer_db,databricks,dataforseo,datagma,deepline_ip_to_company,deepline_native,deeplineagent,discolike,dropleads,emailbison,emailguard,enformion,enigma,exa,findymail,firecrawl,fireflies,firmable,forager,fullenrich,generic_http,gong,google_ads_audiences,google_workspace,govfiles,grain,hackernews,harvestapi,heyreach,hubspot,hunter,icypeas,instantly,intercom,ipqs,kaspr,kernel,leadmagic,lemlist,limadata,linkedin_ads_audiences,linkedin_scraper,luma,lusha,meta_audiences,nooks,openmart,opensosdata,openwebninja,outreach,parallel,peopledatalabs,podscan,postgres,predictleads,prospeo,quickenrich,rapidapi,redshift,rocketreach,salesforce,salesforge,salesloft,scrapecreators,searchbug,sec_edgar,sentrion,serper,slack,smartlead,…." --- # GTM Meta Skill ## Quick Start ```bash 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` commands bare — no pipes, redirection, `2>&1`, command chaining, or backgrounding around them. The CLI already formats, truncates, and prints what you need; `deepline billing usage | head` reads as parsing and loses output. 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. Before the first Deepline fanout in a task, run `deepline preflight --json` as one standalone command and wait for it to finish. Never submit preflight beside another Deepline command. It combines health, authentication, and balance in one process and gives any automatic CLI update a serial boundary. After preflight succeeds, prefix every Deepline command that may run concurrently with `DEEPLINE_SKIP_SELF_UPDATE=1`. This environment prefix is the only exception to the bare-command rule above. Serial commands may stay bare; the opt-out is required for every member of a parallel batch. **Debug every Play run first.** Start a new run with `deepline plays run <play> --input '<json>' --debug`; for an existing run save the complete retained stream with `deepline runs logs <run-id> --out run.log --json`, then use `deepline runs get <run-id> --full --json`. This preserves the durable trail instead of spending on a duplicate run. A caught non-2xx `ctx.fetch` records a customer-safe diagnostic with its call key, method, destination origin, and HTTP status. Generic HTTP is a separate provider surface: inspect the full run package before treating its provider-level error status as the upstream HTTP status or its error body as a safe customer-facing explanation. **Ask for requirements, not implementation instructions.** Requirements are the business outcome, target population, constraints, time horizon, requested destination, and any stated spend or authority boundary. The provider, tool, query shape, identifier recovery, filter expression, fallback order, and workflow structure are implementation decisions. Infer and execute the latter; do not turn them into a questionnaire. When a requirement is genuinely absent and materially changes the result or external action, ask one concise question with a recommendation. Otherwise state a reasonable assumption in the result and keep moving. **Decision-ready communication.** The user should never have to infer the answer from a status update. When work produces people, companies, events, or rows that determine the next move, show those real records in a readable Markdown table first. Link a person's name to their verified LinkedIn profile when one was returned; do not hide the decision behind counts, summaries, or a generic `Profile` column. For choices, show the comparison; for copy, show the draft; for research, show the evidence that supports the conclusion. Then make one plain-language recommendation based on what is visible. State the concrete boundary—who is in, out, or what changes—not a label such as “keep” or “refine.” Do not ask the user to design routine filters or choose plumbing. Keep validation, raw ids, feeds, and tool mechanics internal unless they change scope, cost, risk, confidence, or action. When the user is calibrating, choosing a scope, prioritizing a list, or authorizing a change, use this complete envelope: ```markdown <the table, comparison, draft, or evidence> Recommendation: <one concrete next state and why>. Want me to use that, or adjust it? ``` End with `Want me to use that, or adjust it?` exactly. The user can say “yes” or name the adjustment. A calibration, scope, or prioritization response is incomplete without that final line, even when it starts no external action. Do not add a second question, a menu, or an implementation checklist. If no user decision is needed, state the outcome and stop. **Paid monitors.** Before deployment show the recommended scope, live Deepline price, and delivery in the smallest useful shape. Check Slack first; recommend a real connected channel when one is available, otherwise offer Slack or the configured CRM. The monitor recipe covers consent, history, and similar-company scope. **Discovery order: companies first, then people.** When the task requires finding contacts at companies matching criteria (portfolio, ICP, hiring signal), discover the company set first, then find people at each company. Do not start with broad people-search queries. **Named companies are enough to start.** When the user gives company names but not domains, resolve each canonical company domain before asking them for anything. A domain is a recoverable identifier, not clarification debt. Carry the resolved domain and its official-page evidence into the downstream lookup; do not ask the user to paste a domain list merely because a later tool needs one. Read [finding-companies-and-contacts.md](finding-companies-and-contacts.md) for the identity gate and ambiguity handling. **Known companies + nuanced roles: qualify the real title roster first.** For requests such as "AI leadership at Mount Sinai," "job titles at these companies," or "find the RevOps buyers at these accounts," read and follow [`recipes/find-qualified-titles.md`](recipes/find-qualified-titles.md): `company_titles` -> qualify exact roster titles -> `deepline_native_search_contact` with `title_lists`. Use Exa afterward for public-profile gaps and DropLeads last for supplemental database rows. Broad audience sizing remains a valid DropLeads use case. ### CLI recovery The SDK CLI is the supported CLI. If it is unavailable, run `deepline update` or reinstall it with `npm install -g deepline@latest`; do not switch CLI families. ## 2) Read the matching workflow before execution SKILL.md routes; the matching doc supplies the execution contract. Read it before using a provider so its schema, sequencing, and known failure modes govern the run. **Routing rules — match your task to a doc and READ IT:** | When the task involves... | You MUST read this doc first | What it gives you (that SKILL.md doesn't) | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Finding companies, finding people, building lead lists, prospecting, portfolio/VC sourcing, contact finding at known companies, coverage completion at scale** | [finding-companies-and-contacts.md](finding-companies-and-contacts.md) | Provider filter schemas, parallel execution patterns, provider mix tables, role-based search rules, subagent orchestration, at-scale coverage completion, portfolio/VC shortcuts, contact finding patterns. | | **Researching companies or people, understanding what they build, figuring out use cases, personalizing based on mission/product/industry, enriching a CSV, adding data columns, waterfall enrichment, finding emails/phones/LinkedIn, coalescing data, custom signals, `run_javascript` / `deeplineagent` columns, Apify actors — any task that adds or transforms row-level data** | [enriching-and-researching.md](enriching-and-researching.md) | Play routing per scenario (`deepline plays run` + batch prebuilts + fork/wrap), waterfall orders for email/phone/LinkedIn, `run_javascript` / `deeplineagent` routing inside custom plays, multi-pass pipeline patterns, coalescing, custom signal buckets, Apify actor selection, GTM definitions and defaults. | | **Writing, running, auditing, or modernizing Deepline plays** — composing tools/plays, mapping CSV rows, reviewing an existing `.play.ts` or saved Play for deprecated APIs/tool IDs, fallback logic, joins/projections, durable datasets, custom run/export behavior, or webhook/cron-style orchestration. | [recipes/deepline-plays.md](recipes/deepline-plays.md) | Direct vs compose decisions, `plays check`-driven audits and remediation order, bootstrap/wrap/fork rules, durable authoring basics, run/export/repair routing, and exact SDK/API reference pointers. | | **Writing cold emails, personalizing outreach, lead scoring, qualification, sequence design, campaign copy, inspecting CSVs in Playground.** If the task also requires researching companies/people to inform the writing, read [enriching-and-researching.md](enriching-and-researching.md) too — it has the multi-pass pipeline pattern. | [writing-outreach.md](writing-outreach.md) | Prompt templates from `prompts.json`. Scoring rubrics. Email length/tone/structure rules. Personalization patterns. Qualification frameworks. Playground inspection commands. | | **Deepline Monitors** — continuously capturing a provider's webhook events (email replies, new job postings, intent signals) into a Customer DB table, or deploying/listing/managing those upstream provider pipes. Event-driven streaming, NOT an on-demand enrich/sourcing run. **Conditional gate:** run `deepline monitors status --json` first. Read the recipe only when the command exits 0 with `has_access: true`. Exit 1 with `has_access: false` means rollout access is absent. For exit 3, fix auth/permission; for exit 5, diagnose configuration/server reachability. Do not reinterpret other failures as rollout denial. | [recipes/deepline-monitors.md](recipes/deepline-monitors.md) | What Monitors are, when to use them vs plays, the command set for delivery health, audit/repair, single monitors, and Fleets; the current billing-recovery boundary; monitor definition shape; the provider-webhook → Customer DB → triggered-play data flow; and access gating. | If you are hand-authoring enrich columns instead of using a native play, jump straight to the "Handmade step shape quick reference" section in [enriching-and-researching.md](enriching-and-researching.md). That section spells out the exact runtime contract for `run_javascript`, `extract_js`, `result`, and persisted `matched_result`. ### Recipes: step-by-step playbooks for specific tasks (check before executing) Read the matching recipe before executing. Follow its sequence; adapt it only when the request requires it. | Recipe | Use when... | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `account-orgchart.md` | Building an org chart, account map, buying committee, stakeholder map, or multi-threading plan around a target person or company | | `build-tam.md` | Building a total addressable market list or large company list from ICP criteria | | `clay-to-deepline.md` | Converting a Clay table to Deepline (deprecated enrich-era recipe — use its action mappings, author the result as a custom play) | | `deepline-monitors.md` | **ACCESS-GATED.** Deepline Monitors continuously capture a provider's webhook events into a Customer DB table and trigger plays. Run `deepline monitors status --json` first; only exit 1 with `has_access: false` is a clean rollout denial. Diagnose auth, configuration, and server failures by their actual exit code. | | `deepline-plays.md` | Creating or auditing `.play.ts` and saved Plays, including deprecated APIs/tool IDs, durable datasets, fallback logic, joins/projections, webhook/cron-style orchestration, and custom run/export behavior | | `find-qualified-titles.md` | **Primary path** for nuanced roles at known companies: "AI leadership at Mount Sinai", "find all job titles at these companies", or "find the marketing-ops/RevOps/Salesforce buyers". Pull each company's real title roster (free `company_titles`), qualify exact titles, then find contacts with tiered (LinkedIn, email, phone) reveal. | | `linkedin-url-lookup.md` | Resolving a person's LinkedIn profile URL from their name and company with strict identity validation | | `portfolio-prospecting.md` | Finding companies backed by a specific investor or accelerator, then finding contacts and building personalized outbound | | `small-business-prospecting.md` | Finding local small businesses or storefront/service-area companies using Maps-style search. Doctors, services business, restaurants, etc. | > **Public/social source discovery, community-language pulls, pre-research source planning, or provider-coverage/cost comparison → use the standalone `deepline-research` skill, not a recipe here.** It owns X/Twitter, Reddit, Hacker News, Bluesky, and public-registry fanout plus the source-plan + Deepline-cost synthesis. If none match, grep for more specific keywords: `Grep pattern="<keyword>" path="<directory containing this SKILL.md>/recipes/" glob="*.md" output_mode="files_with_matches"` ### Data - When the user hands you a CSV, run `deepline csv show --csv <path> --summary` first to understand its shape (row count, columns, sample values) before deciding how to process it. - **NEVER read a large CSV into context with the Read tool.** Reading CSV rows into the conversation window exhausts context and produces zero output. This is the single most common failure mode. - For row-by-row processing (enrichment, rewriting, research, scoring), use a Deepline play per §2.5: prebuilt if one fits, fork/wrap when close, author when not. - To explore or understand CSV content without loading it, use `deepline csv show --csv <path> --rows 0:2` for a two-row sample, or spawn an Explore subagent to answer questions about the data. - Pilot before scale: slice the CSV (`head -3 in.csv > pilot.csv`), run the play on the slice, `deepline runs export` and inspect, then run the full file. **Small-input exception:** when an exact-fit prebuilt covers a small input (≤ ~25 rows) whose scope the user already stated, the full file IS the pilot — run it once. - **The pilot is never the deliverable.** A task is not done until the FULL input has run and the result is exported to the exact requested output path: `deepline runs export <run-id> --out "$FINAL_CSV"`. Finishing with only a pilot CSV, or an export under a play-derived name, is the single most common way to fail the task while feeling done. ### Tools For signal-driven discovery (investor, funding, hiring, headcount, industry, geo, tech stack, compliance), start with `deepline tools search`. Do not guess fields. Its syntax is `deepline tools search [query] [--categories <categories>] [--search_terms <terms>] [--json]`: provide a query, or at least one of `--categories` and `--search_terms`. The query is optional only for structured filtering. Use commas for multiple categories or search terms. There is no `--prefix` flag; include a provider name in the query when needed. Search 2-4 synonyms, execute in parallel only after the standalone preflight: ```bash DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search investor DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search "crustdata investor" DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search --categories company_search --search_terms "structured filters,icp" DEEPLINE_SKIP_SELF_UPDATE=1 deepline tools search --categories people_search --search_terms "title filters,linkedin" ``` ## 2.5) Plays are the surface For row-by-row processing (per customer, per lead, per LinkedIn URL), run a Deepline play via `deepline plays run`. `deepline enrich` is deprecated — do not use or document it; when no play fits, author one. 1. **Discover live, then run.** `deepline plays search <query>` and `deepline plays describe <name>` — choose from the live catalog and its contract, never from memory. Search results include a `runCommand` and a `cloneEditStarter` for every prebuilt. 2. **Prebuilt fits** → run it. Batch prebuilts take a CSV directly: `deepline plays run prebuilt/name-and-domain-to-email-waterfall-batch --input '{"csv":"leads.csv"}'`, then `deepline runs export <run-id> --out "$FINAL_CSV"`. 3. **Close but not exact** → pull and edit it. Every prebuilt is forkable: ```bash deepline plays get prebuilt/<name> --source --out ./<name>.play.ts deepline plays check ./<name>.play.ts # mandatory before running deepline plays run --file ./<name>.play.ts --input '{...}' ``` If `plays check` fails on a missing local import, that prebuilt is multi-file — wrap it instead of forking: `deepline plays bootstrap <family> --from <source> --using play:prebuilt/<name> --limit 5 --out workflow.play.ts`. 4. **No play fits** → author one from scratch per [recipes/deepline-plays.md](recipes/deepline-plays.md): compose tools and other plays, map CSVs, add fallback logic and joins. **Results live in the Customer DB.** Every batch play persists its dataset as a durable table: `deepline db query --sql 'select * from "storage"."<table>" limit 20' --max-rows 20 --json` (the run output names the table). Columns are the play's snake_case fields plus per-leg columns like `email_result__hunter_email` — the per-provider audit trail. Rerunning reuses filled cells instead of re-buying them; exports are projections of this table, so nothing is lost if a CSV goes missing. **The iterate loop — pilot, price, fix, then scale:** 1. Run a few rows (slice the CSV or run 2-3 scalar inputs). 2. Read price and performance: `deepline runs get <run-id> --full --json` reports billing and per-step outcomes; the storage table's per-leg columns show which providers hit, missed, or erred. 3. Fix what the pilot exposed BEFORE scaling: a provider that misses or flakes on your segment gets dropped or reordered in a fork; wrong columns get a `columns` map; weak coverage gets a different route. Do not buy the same failure at full scale. 4. Run the full file, export to `FINAL_CSV`, and report to the user: rows delivered, coverage, observed credits, and what you changed after the pilot. ## 3) Core policy defaults ### 3.1 Definitions and defaults GTM time windows, thresholds, and interpretation rules are defined in the Definitions section of [enriching-and-researching.md](enriching-and-researching.md). ## Provider Playbooks Provider-specific playbooks are bundled as separate reference files. Open the relevant playbook when provider-specific behavior, pricing, caveats, or payload conventions matter. [adyntel](provider-playbooks/adyntel.md), [affinity](provider-playbooks/affinity.md), [ai_ark](provider-playbooks/ai_ark.md), [akta](provider-playbooks/akta.md), [allegrow](provider-playbooks/allegrow.md), [amplemarket](provider-playbooks/amplemarket.md), [apify](provider-playbooks/apify.md), [attention](provider-playbooks/attention.md), [attio](provider-playbooks/attio.md), [aviato](provider-playbooks/aviato.md), [bettercontact](provider-playbooks/bettercontact.md), [bigquery](provider-playbooks/bigquery.md), [bloomberry](provider-playbooks/bloomberry.md), [bluesky](provider-playbooks/bluesky.md), [bounceban](provider-playbooks/bounceban.md), [brave](provider-playbooks/brave.md), [browserbase](provider-playbooks/browserbase.md), [builtwith](provider-playbooks/builtwith.md), [clay](provider-playbooks/clay.md), [clickhouse](provider-playbooks/clickhouse.md), [cloudflare](provider-playbooks/cloudflare.md), [contactout](provider-playbooks/contactout.md), [contextdev](provider-playbooks/contextdev.md), [crustdata](provider-playbooks/crustdata.md), [crustdata-v2](provider-playbooks/crustdata-v2.md), [crustdata-v3](provider-playbooks/crustdata-v3.md), [databricks](provider-playbooks/databricks.md), [dataforseo](provider-playbooks/dataforseo.md), [datagma](provider-playbooks/datagma.md), [deepline_native](provider-playbooks/deepline_native.md), [deeplineagent](provider-playbooks/deeplineagent.md), [discolike](provider-playbooks/discolike.md), [dropleads](provider-playbooks/dropleads.md), [emailbison](provider-playbooks/emailbison.md), [emailguard](provider-playbooks/emailguard.md), [enformion](provider-playbooks/enformion.md), [enigma](provider-playbooks/enigma.md), [exa](provider-playbooks/exa.md), [findymail](provider-playbooks/findymail.md), [firecrawl](provider-playbooks/firecrawl.md), [fireflies](provider-playbooks/fireflies.md), [forager](provider-playbooks/forager.md), [fullenrich](provider-playbooks/fullenrich.md), [generic_http](provider-playbooks/generic_http.md), [gong](provider-playbooks/gong.md), [google_ads_audiences](provider-playbooks/google_ads_audiences.md), [govfiles](provider-playbooks/govfiles.md), [grain](provider-playbooks/grain.md), [hackernews](provider-playbooks/hackernews.md), [harvestapi](provider-playbooks/harvestapi.md), [heyreach](provider-playbooks/heyreach.md), [hubspot](provider-playbooks/hubspot.md), [hunter](provider-playbooks/hunter.md), [icypeas](provider-playbooks/icypeas.md), [instantly](provider-playbooks/instantly.md), [intercom](provider-playbooks/intercom.md), [ipqs](provider-playbooks/ipqs.md), [kaspr](provider-playbooks/kaspr.md), [kernel](provider-playbooks/kernel.md), [leadmagic](provider-playbooks/leadmagic.md), [lemlist](provider-playbooks/lemlist.md), [limadata](provider-playbooks/limadata.md), [linkedin_ads_audiences](provider-playbooks/linkedin_ads_audiences.md), [luma](provider-playbooks/luma.md), [lusha](provider-playbooks/lusha.md), [meta_audiences](provider-playbooks/meta_audiences.md), [nooks](provider-playbooks/nooks.md), [openmart](provider-playbooks/openmart.md), [opensosdata](provider-playbooks/opensosdata.md), [openwebninja](provider-playbooks/openwebninja.md), [outreach](provider-playbooks/outreach.md), [parallel](provider-playbooks/parallel.md), [peopledatalabs](provider-playbooks/peopledatalabs.md), [pipedrive](provider-playbooks/pipedrive.md), [podscan](provider-playbooks/podscan.md), [postgres](provider-playbooks/postgres.md), [predictleads](provider-playbooks/predictleads.md), [prospeo](provider-playbooks/prospeo.md), [quickenrich](provider-playbooks/quickenrich.md), [rapidapi](provider-playbooks/rapidapi.md), [redshift](provider-playbooks/redshift.md), [salesforce](provider-playbooks/salesforce.md), [salesforge](provider-playbooks/salesforge.md), [salesloft](provider-playbooks/salesloft.md), [scrapecreators](provider-playbooks/scrapecreators.md), [searchbug](provider-playbooks/searchbug.md), [sec_edgar](provider-playbooks/sec_edgar.md), [sentrion](provider-playbooks/sentrion.md), [serper](provider-playbooks/serper.md), [smartlead](provider-playbooks/smartlead.md), [snowflake](provider-playbooks/snowflake.md), [socrata](provider-playbooks/socrata.md), [sumble](provider-playbooks/sumble.md), [theirstack](provider-playbooks/theirstack.md), [theswarm](provider-playbooks/theswarm.md), [trestle](provider-playbooks/trestle.md), [twitterapi](provider-playbooks/twitterapi.md), [upcell](provider-playbooks/upcell.md), [versium](provider-playbooks/versium.md), [wiza](provider-playbooks/wiza.md), [wizleads](provider-playbooks/wizleads.md), [zerobounce](provider-playbooks/zerobounce.md), [zoho_crm](provider-playbooks/zoho_crm.md), [zoominfo](provider-playbooks/zoominfo.md) - Apply defaults when user input is absent. - User-specified values always override defaults. - In approval messages, list active defaults as assumptions. ### 3.2 Working directory — set up BEFORE any file writes **NEVER write files to `/tmp/` or any absolute temp directory.** Files in system `/tmp/` are wiped on reboot — users permanently lose enriched CSVs, research outputs, and hours of paid enrichment work. This is a critical data-loss risk. Set up a descriptive project-local working directory as your first action: ```bash WORKDIR="deepline/data/<descriptive-task-slug>" && mkdir -p "$WORKDIR" && echo "$WORKDIR" ``` The slug must describe the task (e.g. `deepline/data/yc-cmo-outbound`, `deepline/data/acme-email-waterfall`). Do NOT use random names like `mktemp` generates — the user needs to find these files later. See [enriching-and-researching.md](enriching-and-researching.md) for full details. ### 3.3 Output policy and User Interaction Pattern - Always use a Deepline play for list enrichment or discovery at scale (>5 rows) — §2.5 routing. The run's play page lets the user inspect rows and rerun; send that URL. - Even for company → ICP person flows, plays work: search and filter as part of the process, with providers like Apify to guide. - Even when you don't have a CSV, create one and run the batch play against it. - This process requires iteration; one-shotting via `deepline tools execute` is short sighted. - In chat, send file/run links and render a decision table when rows inform a decision. - Preserve lineage columns (especially `_metadata`) end-to-end. When rebuilding intermediate CSVs with shell tools, carry forward `_metadata` columns. - Never overwrite a user-provided source CSV; write outputs to your working directory. Reruns of a play reuse completed cells by default. See [enriching-and-researching.md](enriching-and-researching.md) for `deepline csv` commands, pre-flight/post-run script templates, and inspection details. ### 3.4 Final file and run check (light) - Keep one intended final CSV path: `FINAL_CSV="${OUTPUT_DIR:-$WORKDIR}/<requested_filename>.csv"` - Before finishing: use the post-run inspection script pattern from [enriching-and-researching.md](enriching-and-researching.md). Run it once instead of separate checks. - **Checkpoint the deliverable.** On multi-phase pipelines (companies → contacts → emails), write `FINAL_CSV` as soon as the first complete rows exist and overwrite it as later phases improve it. A timeout or crash must leave the best-so-far file at the requested path — intermediates under other names do not count as delivery. - **For a task that ran a Play, include its result.** Give the exact `FINAL_CSV` path and play page link; do not invent one for monitor-only, research, or advisory work. ## 4) Credit and approval gate (paid actions) This section's pilot, CSV preview, and full-run template governs enrichment, sourcing, and other row-processing runs. Monitor mutations use the workflow in `recipes/deepline-monitors.md` instead: inspect scope, reuse candidates, downstream actions/unknown consumers, and price before asking. Keep that approval decision-first: scope table, recommendation, live Deepline price, and one question. Because a monitor can incur variable future event charges, require explicit approval before every paid deploy, reactivate, or historical widening—even when the user stated the scope. A historical rung is empty only after its documented provider completion window; leave it intact while that window is pending. Ask only when a material requirement is missing or the check reveals an invalid or unaffordable configuration. ### 4.1 Required run order 1. Pilot on a narrow scope: a small CSV slice through the same batch play, or 2-3 scalar runs. 2. If the scope is NOT already approved (see below), request explicit approval. 3. Run full scope, report the pilot's cost and quality findings alongside the deliverable. **User-stated scope = already approved for bounded row-processing only.** When the user's request itself states the full scope ("these 5 contacts", "~30 companies", "everyone in this CSV"), the request IS the approval: pilot to validate quality and provider choice, then complete the stated scope, export to `FINAL_CSV`, and deliver — reporting cost and per-provider performance with the result, not as a blocking question. This exception never applies to monitors; follow the monitor-specific consent gate above. **Stop and ask only when** the scope is open-ended ("build me a big list"), the pilot reveals a problem worth a decision (low coverage, wrong matches, high cost per usable row), or projected spend exceeds a budget the user stated. Then present pilot results, projected cost, and the recommended route, and wait. ### 4.2 Execution sizing - Use smaller sequential commands first. - Keep limits low and windows bounded before scaling. - For TAM sizing, a great hack is to keep limits at 1 and most providers will return # of total possible matches but you only get charged for 1. - Prefer providers and plays that charge on returned results or successful hits when coverage is uncertain. If a provider bills per attempt/request/page, prove quality on a tiny pilot before letting it fan out. - Stop after the pilot when the first rows show low usable coverage, wrong-person/company matches, missing getters, or high cost per usable row. Change route/provider order before buying the same failure at full scale. - Do not depend on monthly caps as a hard risk control. - Plan play spend before full scale by reading the play contract, using `deepline plays list --show-cost` for its static or observed per-row/per-run estimate when available, then running a small pilot and inspecting actual spend with `deepline runs get <run-id> --full --json`; label any extrapolation as an estimate. On `plays share publish` and `plays share update`, `--show-cost` instead opts into average cost metrics on the public page when safe, comparable run samples are available. `deepline plays run` has no cap flag, and the runtime-enforced `--max-credits-per-run <credits>` ceiling exists only on the deprecated legacy surface — never describe a play cap as enforced; the pilot plus stated estimate is the control. ### 4.2.1 Over-provision, then filter — never chase missing rows When the user asks for N rows, start with ~1.4×N (e.g., 35 for 25). Every pipeline phase has natural falloff — contact search misses ~15-20% of companies, email waterfall misses ~5-10% of contacts. Fighting to complete the hard rows is almost always a waste: the companies that providers can't find contacts for are the same ones that won't have email coverage either. **Do this:** 1. Pull more candidates than needed at the top of funnel. 2. Run the full pipeline (contacts → emails → outbound). 3. At the end, filter to the best N complete rows and deliver those. 4. Drop incomplete rows — don't retry or manually patch them. **Do NOT do this:** - Trim results to exactly N before running the pipeline. - Spend turns retrying failed lookups with fallback providers, `deeplineagent` research passes, or manual patching. - Run enrichment on all rows just to fill gaps in a few (especially broad `deeplineagent` research passes). Provider coverage is a property of the company, not something you can overcome with more effort. Tiny startups with 5 people will have zero coverage across all providers — no amount of retrying changes that. Over-provision at the top and let incomplete rows fall off naturally. ### 4.3 Approval message content Include all of: 1. Play or provider(s) 2. Pilot summary and observed behavior 3. Intent-level assumptions (3–5 one-line bullets) 4. CSV preview from the real pilot: the head of `deepline runs export <pilot-run-id> --out` 5. Credits estimate / range 6. Full-run scope size 7. Max spend cap (stated and monitored; no runtime-enforced play cap exists) 8. Approval question: `Approve full run?` Strict format contract (blocking): 1. Use the exact four section headers: Assumptions, CSV Preview (ASCII), Credits + Scope + Cap, Approval Question. 2. If any required section is missing, remain in `AWAIT_APPROVAL` and do not run paid/cost-unknown actions. 3. Only transition to `FULL_RUN` after an explicit user confirmation to the approval question. 4. `run_javascript` is the non-AI path. `ai_inference` is for general classification/structured reasoning, and `deeplineagent` is for context gathering / web research / signal extraction. Approval template: ```markdown Assumptions - <intent assumption 1> - <intent assumption 2> CSV Preview (ASCII) <paste verbatim pilot output: the runs export head> Credits + Scope + Cap - Provider: <name> - Estimated credits: <value or range> - Full-run scope: <rows/items> - Spend cap: <cap> - Pilot summary: <one short paragraph> Approval Question Approve full run? ``` ### 4.4 Mandatory checkpoint - Must run a real pilot against the exact CSV intended for the full run: a small slice through the same batch play. - Must include the pilot output preview verbatim in approval. - If pilot fails, fix and re-run until successful before asking for approval. - Ask for approval in chat after the pilot. Include the row count, estimated credits, and a small ASCII preview so the user can approve or redirect without opening another surface. ### 4.5 Billing commands ```bash deepline billing balance # Show current credit balance deepline billing usage # Show recent billing activity and grouped recent usage deepline billing limit # Show the current monthly billing cap ``` When credits are zero or unavailable, stop paid work and ask whether the user wants to add Deepline credits. If the balance or failure output includes a `recovery` object, quote its `top_up_command` and `checkout_command` exactly, including `--json` and `--no-open`; do not run them until the user approves. Do not hardcode a USD-to-credit exchange rate in the skill. Use live billing, pricing, or tool output when quoting credit costs. ## 5) Provider routing (high level) **Quick-reference summary only — the Section 2 sub-doc you already read is the authority.** - **Search / discovery** → You MUST have [finding-companies-and-contacts.md](finding-companies-and-contacts.md) open. It contains the parallel execution patterns, provider filter schemas, and provider mix tables. Start with `deepline tools search <intent>` and execute field-matched provider calls in parallel; when the `deepline-list-builder` subagent is available, use subagent-based parallel search orchestration as the preferred pattern. Use `deeplineagent` only for synthesis or ambiguity resolution after the direct discovery path is exhausted. - **Enrich / waterfall / coalesce** → You MUST have [enriching-and-researching.md](enriching-and-researching.md) open. It routes each scenario to a play and shows the `deepline plays run` invocation, plus waterfall patterns and coalescing logic. Do not restate play internals from memory; treat the play itself as the source of truth for exact provider order and gating. - **Custom signals / messaging** → Read [enriching-and-researching.md](enriching-and-researching.md) (custom signals section). Use `run_javascript` for deterministic transforms/template logic and `deeplineagent` for AI work. Start from `prompts.json`. - **Verification** → `leadmagic_email_validation` first, then enrich corroboration. - **LinkedIn profiles, company employees, posts, comments, and reactions** -> Prefer Deepline's native HarvestAPI provider. Use the documented `harvestapi_*` names as starting hints, then run `deepline tools describe <operation> --schema-only` before execution; broad tool search can be noisy. Use Apify only when HarvestAPI does not expose the required LinkedIn surface. - For phone recovery, read [enriching-and-researching.md](enriching-and-researching.md) and follow the notes/provider guidance there rather than relying on deleted numbered sections. Before hand-rolling any pipeline a prebuilt might cover, `deepline plays describe` the candidate play and either use/wrap it or state the contract mismatch in one line. Silently bypassing a fitting prebuilt is a routing failure. Provider path heuristics: - Broad first pass: direct tool calls for high-volume discovery. - Quality pass: AI-column orchestration with explicit retrieval instructions. - For job-change recovery: prefer quality-first (`crustdata_v3_person_enrich`, `peopledatalabs_*`) before `leadmagic_*` fallbacks. - Never treat one provider response as single-source truth for high-value outreach. ## 6) Additional notes Critical: keep [writing-outreach.md](writing-outreach.md) workflow context active when running any sequence task. It is not optional for ICP-driven messaging. ### Operational troubleshooting: rate limits and CLI health - Use Deepline plays for heavy row-by-row work whenever possible. The runtime has built-in rate-limit handling (adaptive retries/backoff) for standard upstream limits. If you are building a homegrown script, assume it does not include the same automatic protection unless you explicitly implement it. - If enrichment or CLI behavior is unstable, update the CLI and reinstall the Deepline skills: ```bash deepline update deepline skills ``` **Sites requiring auth:** Don't use Apify. Tell the user to use Claude in Chrome or guide them through Inspect Element to get a curl command with headers (user is non-technical). 1. If user provides actor ID/name/URL: use it directly. 2. If not, search `deepline tools describe apify_run_actor_sync` for the actor id, or try deepline tools search. 3. If not present, run discovery search. 4. Avoid rental-priced actors. 5. For LinkedIn posts and engagers, use the native HarvestAPI operations first: inspect `harvestapi_search_posts`, `harvestapi_get_post`, `harvestapi_get_post_reactions`, and `harvestapi_get_post_comments`. Use `supreme_coder/linkedin-post` only when the native provider does not cover the requested shape. Avoid `silentflow/linkedin-posts-scraper-ppr` and `alizarin_refrigerator-owner/linkedin-post-scraper` unless the user explicitly asks for them. 6. Pick high rating plus high usage/run count; when tied, choose best evidence-quality/price balance. 7. Honor `operatorNotes` over public ratings when conflicting. ```bash deepline tools execute apify_list_store_actors --input '{"search":"similarweb traffic scraper","sortBy":"relevance","limit":20}' deepline tools execute apify_get_actor_input_schema --input '{"actorId":"radeance/similarweb-scraper"}' ``` ## 7) Feedback & session sharing ### 7.1 Proactive issue reporting (mandatory) Without waiting for a request, run `deepline feedback send` once per issue cluster when calls repeatedly fail, output is clearly wrong, a bug blocks work, a workaround is substantial, a Play is disproportionately slow/stuck, or an ordinary result takes avoidable product steps. Then continue the user's task. Include the goal, tool/provider/model, failure and exact error, and attempted reproduction. For latency or excess steps, include the run ID, input size, duration/polling or actual path, and expected progress/direct path. Do not report legitimately long provider work. For errors, pass `--error-outcome terminal` if work stopped or `--error-outcome continued` if work continued. Omit it for non-errors. ```bash deepline feedback send "Goal: <goal>. Tool: <details>. Failure: <error and repro>." --error-outcome <terminal|continued> ``` ### 7.2 End-of-session consent gate (mandatory) After the substantive result is resolved, ask exactly one Yes/No question in a separate message—never beside a table, recommendation, approval request, or unresolved user decision. Do not ask it after a terminal no-change outcome: `Change: none — I’m leaving it as is.` must remain the final line of that response. `Would you like me to send this session activity to the Deepline team so they can improve the experience? (Yes/No)` If user says: - **Yes** -> run: ```bash deepline sessions send --current-session --rating <good|bad|neutral> ``` - **No** -> do not send the session. Ask once per completed run; do not re-ask before a new run/session. Rate by outcome, not tone (see `--help`). -
writing-outreach.md 12.7 KB
# Writing Outreach Skill Use this skill when the task involves cold emails, personalization, lead scoring, qualification, sequence design, campaign copy, or inspection of enrichment results. ## What This Skill Does - Loads ICP/criteria context from a local file (for example `./icp.md`) using `read_file`. - Produces strict, structured qualification output: - numeric score (`0-10`) - fit label (for example `Strong fit: 8/10`) - rationale summary - question-by-question yes/no/unknown answers with confidence and rationale - Produces strict, structured 4-step outbound email sequence: - subject - core value prop - message body - sequence-level rationale ## Required Inputs - `prospect_payload`: Person + company context (JSON string/object in row) - `icp.md` (or equivalent): Local qualification criteria and positioning constraints ## General Setup (Recommended) Create a small context pack in your working directory before running this flow: - `icp.md`: Ideal customer profile and disqualifiers - `qualification_questions.md`: The exact questions you want scored - `product_context.md`: Value props, proof points, and constraints - `copy_rules.md`: Tone, banned claims, length limits, CTA style Minimal folder layout: ```text ./context/ icp.md qualification_questions.md product_context.md copy_rules.md ``` Example `icp.md` starter: ```markdown # ICP ## Best-fit companies - B2B SaaS, 200-5000 employees, multi-product GTM team - Uses modern data stack and CRM-based workflows ## Best-fit personas - VP/Head/Director in Marketing, RevOps, Sales Ops, GTM Ops - Owns pipeline quality, segmentation, scoring, or campaign orchestration ## Core pains - Slow GTM iteration due to analyst/engineering dependency - Low trust in black-box scoring - Weak signal-to-action workflow for reps ## Disqualifiers - <20 employees - Pure B2C motion - No clear sales/marketing operations function ``` ## Best Practices - Keep `icp.md` specific and opinionated. Broad ICPs create generic copy. - Separate facts vs assumptions in your context docs. - Put proof points in `product_context.md` so emails can reference them cleanly. - Define hard copy constraints in `copy_rules.md`: tone, max words, banned phrases, allowed CTA types. - Use `Unknown` for missing evidence in qualification instead of guessing. - Run the QA prompt template before finalizing sequence copy. - Version your context docs with the campaign (`context-q1-enterprise.md`, etc.). ## Output Contracts ### Qualification JSON Contract ```json { "data": { "score": 8, "score_label": "Strong fit: 8/10", "fit_band": "STRONG_FIT", "rationale": "Short summary of fit and caveats.", "qualification": { "answers": [ { "question": "string", "answer": "Yes", "confidence": "HIGH", "rationale": "string" } ], "summary": { "positives": ["string"], "risks": ["string"], "next_checks": ["string"] } } } } ``` ### Email Sequence JSON Contract ```json { "data": { "emails": [ { "step": 1, "subject": "string", "coreValueProp": "string", "email": "string" } ], "sequence_rationale": "string" } } ``` ## Personalization: `run_javascript` vs `deeplineagent` When you already have contact + company context in CSV columns, use `run_javascript` for email generation when a deterministic template is enough. Use `deeplineagent` when you want AI help with reasoning, scoring, copy quality, or research. **Critical: avoid mail-merge output.** If every email has the same structure with only `{{first_name}}` and `{{company_name}}` swapped, it's a template — not personalized outreach. Each email must reference something specific to the company (product, use case, industry, recent news). Use `company_description`, `one_liner`, `company_research`, or other enrichment columns in your JS template or `deeplineagent` prompt so each email is substantively different. Author the generation column(s) as a custom play over your enriched CSV ([recipes/deepline-plays.md](recipes/deepline-plays.md)) with one `withColumn` per output: - Fast path: a `run_javascript` column applying a deterministic template. - AI path: a `deeplineagent` column with the research columns interpolated into the prompt and a `jsonSchema` of `{subject, email}`. - Research-first path: a research `deeplineagent` column, then the generation column consuming it. Notes: - `{{company_research}}` is the safe form for AI prompts when the prior column came from `deeplineagent`. - If you need a single field from prior research for deterministic template logic, extract it into its own scalar column with `run_javascript` first, then reference that scalar column in later steps. - Inside `run_javascript`, use `row["company_research"]` or `row.company_research`. Do not use `{{row.company_research}}`; `row` is only available inside the JS code. ## Recommended Workflow ```bash deepline tools describe deeplineagent ICP_CONTEXT=$(cat ./context/icp.md) PRODUCT_CONTEXT=$(cat ./context/product_context.md) QUAL_WITH=$(jq -nc --arg icp "$ICP_CONTEXT" '{ alias: "qualification_output", tool: "deeplineagent", payload: { model: "openai/gpt-5.4-mini", prompt: ("You are a B2B qualification analyst. Use only the provided evidence. Inputs:\nICP context:\n" + $icp + "\nProspect payload:\n{{prospect_payload}}"), jsonSchema: { type: "object", properties: { data: { type: "object", properties: { score: { type: "number" }, score_label: { type: "string" }, fit_band: { type: "string" }, rationale: { type: "string" } }, required: ["score", "score_label", "fit_band", "rationale"], additionalProperties: false } }, required: ["data"], additionalProperties: false } } }') SEQ_WITH=$(jq -nc --arg product "$PRODUCT_CONTEXT" '{ alias: "email_sequence_output", tool: "deeplineagent", payload: { model: "openai/gpt-5.4-mini", prompt: ("You are a B2B email strategist. Write 4 concise emails tied to the qualification output and product context.\nProduct context:\n" + $product + "\nQualification payload:\n{{qualification_output}}\nProspect payload:\n{{prospect_payload}}"), jsonSchema: { type: "object", properties: { data: { type: "object", properties: { emails: { type: "array", items: { type: "object", properties: { step: { type: "number" }, subject: { type: "string" }, coreValueProp: { type: "string" }, email: { type: "string" } }, required: ["step", "subject", "coreValueProp", "email"], additionalProperties: false } }, sequence_rationale: { type: "string" } }, required: ["emails", "sequence_rationale"], additionalProperties: false } }, required: ["data"], additionalProperties: false } } }') printf "prospect_payload\n{\"person\":{\"firstName\":\"Rachael\",\"lastName\":\"Foster\",\"title\":\"Vice President AMER Field Marketing, Public Sector, Services, & Community\"},\"company\":{\"name\":\"Cloudera\",\"domain\":\"cloudera.com\"}}\n" > ./qualification_email_seed.csv # Run both columns as a custom play over the seed CSV (one withColumn per # payload above) — recipes/deepline-plays.md has the authoring loop. ``` ## Prompt Templates (Copy/Paste) Use these as `prompt` values inside `deeplineagent` payloads. Pair each prompt with a `jsonSchema` that matches the output contracts above. Recommended starting model: `openai/gpt-5.4-mini`. ### 1) Score + Fit Label Template ```json { "prompt": "You are a B2B scoring analyst.\nReturn strict JSON only:\n{\"data\":{\"score\":0,\"score_label\":\"\",\"fit_band\":\"\",\"scoring\":{\"weights\":[{\"factor\":\"\",\"weight\":0,\"evidence\":\"\",\"impact\":\"positive|negative|neutral\"}],\"confidence\":\"HIGH|MEDIUM|LOW\"},\"rationale\":\"\"}}\nRules:\n- score is integer 0-10\n- score_label format: \"Strong fit: X/10\" or \"Possible fit: X/10\" or \"Weak fit: X/10\"\n- fit_band one of: STRONG_FIT, POSSIBLE_FIT, WEAK_FIT\n- use evidence from provided context only; do not invent facts\n- default to higher recall unless strict matching is explicitly requested\nInputs:\nICP context: {{icp_context}}\nProspect payload: {{prospect_payload}}", "model": "openai/gpt-5.4-mini" } ``` ### 2) Qualification QA + Rationale Template ```json { "prompt": "You are an ICP qualification analyst.\nReturn strict JSON only:\n{\"data\":{\"qualification\":{\"answers\":[{\"question\":\"\",\"answer\":\"Yes|No|Unknown\",\"confidence\":\"HIGH|MEDIUM|LOW\",\"rationale\":\"\"}],\"summary\":{\"positives\":[\"\"],\"risks\":[\"\"],\"next_checks\":[\"\"]}},\"rationale\":\"\"}}\nRules:\n- keep answers short and explicit\n- each rationale must reference concrete evidence from context\n- mark Unknown when evidence is missing\n- avoid strict/exact matching unless explicitly asked\nInputs:\nICP questions: {{qualification_questions}}\nICP context: {{icp_context}}\nProspect payload: {{prospect_payload}}", "model": "openai/gpt-5.4-mini" } ``` ### 3) 4-Step Email Sequence Design Template ```json { "prompt": "You are a B2B email strategist.\nReturn strict JSON only:\n{\"data\":{\"emails\":[{\"step\":1,\"subject\":\"\",\"coreValueProp\":\"\",\"email\":\"\"}],\"sequence_rationale\":\"\"}}\nRules:\n- exactly 4 emails with step 1..4\n- each email must map to a pain or risk from qualification summary\n- concise style, no fluff, no markdown\n- personalization must reference role/company context from prospect payload\n- avoid claims not supported by inputs\nInputs:\nProduct context: {{product_context}}\nQualification output: {{qualification_output}}\nProspect payload: {{prospect_payload}}", "model": "openai/gpt-5.4-mini" } ``` ### 4) Subject Line Variant Generator Template ```json { "prompt": "You are a B2B subject line writer.\nReturn strict JSON only:\n{\"data\":{\"subjects\":[{\"variant\":\"\",\"angle\":\"pain|outcome|proof|curiosity\",\"why_it_matches\":\"\"}]}}\nRules:\n- create 8 variants max 7 words each\n- no clickbait, no ALL CAPS, no exclamation marks\n- tie each variant to qualification rationale\nInputs:\nQualification output: {{qualification_output}}\nProspect payload: {{prospect_payload}}", "model": "openai/gpt-5.4-mini" } ``` ### 5) Email Quality Critique Template (Optional QA Pass) ```json { "prompt": "You are a cold email QA editor.\nReturn strict JSON only:\n{\"data\":{\"issues\":[{\"step\":1,\"severity\":\"HIGH|MEDIUM|LOW\",\"issue\":\"\",\"fix\":\"\"}],\"revised_emails\":[{\"step\":1,\"subject\":\"\",\"coreValueProp\":\"\",\"email\":\"\"}]}}\nRules:\n- flag vague claims, unsupported assertions, and weak personalization\n- keep original structure and tighten only where needed\nInputs:\nQualification output: {{qualification_output}}\nEmail sequence output: {{email_sequence_output}}\nProspect payload: {{prospect_payload}}", "model": "openai/gpt-5.4-mini" } ``` ## Guardrails - Keep qualification deterministic and evidence-based from provided context. - Prefer high recall unless strict matching is explicitly requested. - Keep outputs strict JSON (no markdown wrappers). - Keep email copy concise, specific to role/company context, and grounded in qualification rationale. ## CSV Inspection Use these commands to interact with `deepline csv` directly for inspecting and debugging enrichment results. ### Open an existing CSV ```bash deepline csv render --csv leads.csv --open ``` - Use `--open` to launch the CSV viewer. ### Inspect rows (`deepline csv show`) `deepline csv show --csv <path> [--format json|table|csv] [--verbose] [--summary] [--rows START:END]` - format: json (default, `{rows, _metadata}`), table (ASCII, 40-char cap), csv (RFC 4180) - --verbose: include step columns + full cell values - --summary: per-column stats + miss_reasons - --rows: `start:end` bounds (default `0:19`) ```bash deepline csv show --csv leads.csv deepline csv show --csv leads.csv --format table --rows 0:10 deepline csv show --csv leads.csv --summary ``` ### Re-run a column `deepline csv` is local inspection only. To recompute a column, re-run the play that produced it: completed cells are reused from the durable dataset by default, and `deepline plays run --force` starts a fresh run graph when you genuinely want everything recomputed. ### CLI-only debug posture - If you need to inspect or re-execute, use these CSV commands directly. - If you need to add columns or add providers, switch back to [enriching-and-researching.md](enriching-and-researching.md) instead of extending this page.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.