Use this skill for any GTM Engineering work in Deepline — prospecting, list-building, account research, contact enrichment, email/phone/LinkedIn lookup, ICP qualification, lead scoring, outreach copy, CSV-driven row work, and verifying enriched data. Triggers on phrases like 'enrich this CSV', 'find contacts at these companies', 'build a TAM list', 'waterfall emails for these leads', 'detect job changes', 'is this data accurate', 'write a sequence for', and on any request that mentions Deepline, plays, or named GTM providers (Crustdata, Hunter, Dropleads, etc.). Use this even when the user does not explicitly say 'GTM' — most CSV-with-leads tasks and most provider-driven enrichment tasks are GTM tasks. SKIP only when the request is a Clay table extraction (use clay-to-deepline) or has no Deepline / outbound / data-enrichment dimension at all.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
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 -hDeepline is a TypeScript SDK and CLI for GTM execution: durable, typed workflows (plays) that call providers, create row datasets, run waterfalls, validate, and produce CSVs. The job: take an ICP and turn it into an enriched, scored dataset the customer trusts enough to act on. Every AI-filled or provider-filled cell is a claim until verified, so correctness is a phase of the work, not an afterthought.
Names of plays and tools are starting hints. The CLI is the live source of truth.
Tool IDs and play names get renamed, deprecated, and added constantly. Confirm any name before spending:
deepline tools list --json— enumerate the tool categories;deepline tools list <category> --jsonlists EVERY tool in one category. Browse first: enumerate categories → exhaustive category listing →describe. This is complete recall — use it before concluding a provider class doesn't exist.deepline tools search <query> --json— ranked search, for a cross-cutting filter, not an inventory.deepline plays search <category> --json— find the current canonical play for a pattern.deepline plays describe <name> --json/deepline tools describe <id> --json— confirm the input contract before invoking.- Prefer
prebuilt/<name>refs over a plain play name. A plain name can resolve to a stale workspace-owned copy the runtime rejects ("legacy runtime contract… Republish this play"), whosedescribemay advertise a wrong input key. That error is a config artifact, not a real failure — re-run against theprebuilt/ref.When unsure about command shape, run
deepline <command> --helpbefore guessing flags.
This is the spine. Every provider-backed request runs these four stages in order. The failure mode it fixes: an agent classifies a familiar task as "known," jumps straight to a prebuilt, and never measures the alternatives, so a lone default provider's coverage becomes the customer's ceiling, unexamined. Explore first, always. A prebuilt is a candidate you score, never a reason to skip scoring.
1. EXPLORE. One parallel pass, not a serial probe. Pilot 3-5 rows across every viable route at once. Fork plays/route-fanout.play.ts in search mode (mode:'search'), set strategy, and list the candidate routes for the field. A category is complete recall (deepline tools list <category> --json), not a famous-name shortlist. Prebuilts enter here as candidates, compared head to head with the raw routes; they never bypass the bake-off. This is FASTER than jumping to a prebuilt, not slower: N routes fire concurrently, so you measure the whole market at the wall-clock of a single route. Uncredentialed bring-your-own providers (labeled requires_connection in tools describe/list) are NOT candidates. Surface "connect X to add this route" instead; do not bake off a route that fails closed and scores a false 0.
2. SCORE. Rank the routes, cheap judge only. Rank each route by three numbers. Coverage: read the native dataset_execution_stats[<column>].non_empty fill rate from deepline runs get <id> --full --json, never a hand-tallied count. Correctness: against golden truth where you have it (shared/correctness.md). Marginal cost per row: credits/row at scale, never the pilot's inflated total. Any model-side ranking or tie-break uses the cheap Haiku-class judge, the batched rerank subagent (plays/shared/rerank.ts / plays/shared/rerank-cli.ts). NEVER deeplineagent, never the expensive main model for grunt scoring (LAW 6). Emit a compact route scorecard: route × coverage × correctness × cost/row.
3. EXPLOIT. Run the winner at production size. Take the winning route or fused waterfall to the full row set. A covered pattern is a prebuilt (prebuilt/<name>) or the fused route-fanout winner in exploit mode (mode:'exploit', the default); a genuinely novel composition is a hand-authored *.play.ts (the SLOW path; see § Surface lifecycle). Discover the exact current play name and confirm its contract before spending (deepline plays search, plays describe), then run it.
4. VERIFY. Validate, export, report provenance. Validate the filled cells (shared/correctness.md), export to a user-controlled path, and report the native coverage number (dataset_execution_stats[<column>].non_empty, formatted "N/M (X%)") plus per-route provenance. A number without a source is a claim, not data (LAW 7).
The single-route exception (skip EXPLORE): a genuinely closed, single-value task, like validate ONE email or look up ONE known domain. That is the only reason to skip the bake-off. Everything provider-backed with coverage uncertainty (discovery, contact recovery, research) enters at EXPLORE. "Familiar" is not "closed": "find CTOs + emails" feels known and still has a coverage ceiling you have not measured, so it explores.
Optimize in one order across all four stages: trust, then correctness, then price. Never trade the first two for the third. Trust is the session itself (route map up front, real rows early, a scoreboard at each checkpoint); correctness is whether the cells survive verification; price is the refactor-phase concern the golden loop handles last. The standing question, asked on every route and after every delivery: can this be more trustworthy, more accurate, cheaper?
The failing case. It feels known, so the old flow ran a prebuilt and never explored. Under the spine it enters at EXPLORE.
EXPLORE. Discovery has two legs. The email/contact-recovery leg bakes off cleanly in the scorer: fork route-fanout in search mode and fire the email routes concurrently on a 3-row pilot with known-truth emails.
deepline plays run plays/route-fanout.play.ts \
--input '{"strategy":"email","mode":"search"}' \
--csv pilot.csv --watchPilot rows carry an email__truth column so the scorer measures each route against real answers. The DISCOVERY leg (which people-search provider finds the CTOs at each domain) is a lighter multi-route pilot for now, not the same one-shot bake-off. Name it honestly and pilot the 2-3 candidate people-search providers over the same 3 rows rather than pretending it's frictionless.
SCORE. Read dataset_execution_stats[email].non_empty from deepline runs get <id> --full --json for each route; check correctness against the email__truth column; quote marginal cost/row. The Haiku judge (rerank-cli.ts) breaks ties on the discovery leg. The scorecard:
| route | coverage (non_empty) | correct vs truth | cost/row |
|---|---|---|---|
| finder + validator waterfall | 3/3 (100%) | 3/3 | ~4 cr |
| people-search aggregator | 2/3 (67%) | 2/2 | ~6 cr |
| finder-only, no validator | 3/3 (100%) | 2/3 | ~2 cr |
The waterfall wins: full coverage AND correct, and the validator is worth its marginal cost because finder-only shipped one wrong email.
EXPLOIT. Run the winning waterfall on the full 10.
deepline plays run plays/route-fanout.play.ts \
--input '{"strategy":"email","mode":"exploit"}' \
--csv ctos.csv --watchVERIFY. Validate the fills, export to the user's path, report coverage from the native stat: "9/10 work emails (90%), each with its resolving route and validation verdict; the one miss is an honest null with a reason." Provenance travels with every cell.
tools describe, plays describe). Dynamic ctx.tools.execute refs are NOT preflight-checked — a wrong id fails only at run time.deeplineagent, never inside the durable play (shared/reranking.md).dataset_execution_stats[<column>].non_empty (fill rate, formatted "N/M (X%)") from deepline runs get <id> --full --json. Do not recompute a scorecard by hand. When it's low, don't ship a thin result silently — proactively offer (or, when it's clearly inadequate, auto-escalate to) a wider provider fan-out. A cheap single-provider result at 40% coverage is a finding, not a delivery. See jobs/enriching.md § Start cheap, measure coverage, escalate.*.play.ts — a blank-LinkedIn data-shape bug, a row-key repair, three edit→preflight→run loops. The enrichment itself was seconds. Accuracy was perfect; speed was the failure. A prebuilt (or a route-fanout sweep) would have skipped the debug loop entirely. Reach for authoring last, at EXPLOIT, only for a genuinely novel composition (see § The lifecycle).name-and-domain-to-email-waterfall hit "legacy runtime contract… Republish this play" — a stale workspace-owned copy whose describe advertised a wrong input key. It is a config artifact, not a data failure. Re-run against the prebuilt/<name> ref.serper_search (nonexistent; the real id is serper_google_search) — preflight passed, it errored only at run time. Confirm every route's tool id with tools describe before shipping a route (LAW 4).hunter/findymail/prospeo/fullenrich/exa were uncredentialed and short-circuited to 0ms. Check which routes actually executed before concluding coverage is poor.company-to-contact returns identity only (email: null always) — you must chain the email waterfall. Don't read all-null as "no emails found."verify_next while letting one wrong answer through. Unshipped ≠ wrong and shipped ≠ all-correct — read the tags, and re-check borderline rows before concluding.ProviderTransientError to continue a read waterfall. Never turn validation, authentication, billing, Deepline, or unknown failures into nulls. Keep the final waterfall leg loud. The governor owns concurrency, not you.All surfaces hit the same backend. Use the CLI to discover, invoke, inspect, promote. Use a prebuilt play when the registry already has the business pattern — reference it as prebuilt/<name> (a plain name can shadow a stale copy; see the CLI callout above). Use a custom *.play.ts only when the work has multiple durable boundaries no prebuilt covers: source rows, provider calls, datasets, validation, scoring, branching, export shape, reruns. Direct tools execute calls are probes, not pipelines. Use the programmatic client (Deepline.connect()) only from external Node apps, never inside a play body.
Custom-play lifecycle (the SLOW path): write my-play.play.ts with definePlay → run locally (deepline plays run ./my-play.play.ts --input '{...}' --watch) → inspect and edit the same file (the play accumulates checkpointed stages, so reruns reuse completed work) → promote when stable (deepline plays set-live ./my-play.play.ts), then invoke by name or via ctx.runPlay.
Keep the top-level play description concise. The description in definePlay(..., { description }) becomes the primary title shown in the UI. Write a 2–6 word outcome phrase, at most 48 characters, with no trailing period. Do not recap the request or implementation; use Refresh provider status, not Find all providers that need to be updated and refresh their status. This rule applies to the play description, not the kebab-case play name or descriptions for tools, steps, and datasets.
If it touches a provider, it belongs in play code — probes, parallel fan-outs, waterfalls, research columns, exports. Only the play gets durability, receipts, a runtime sheet, and governed parallelism; a bare tools execute is for sniffing one contract, and a shell loop of them is the anti-pattern. Fire independent calls concurrently with ordinary Promise.all — the runtime still owns rate limits, retries, and billing, so parallelism costs nothing but the wall-clock it saves.
The cache makes iteration nearly free, so build like it. Every tool call writes a content-addressed receipt (tool + input; misses included) and every filled cell is reused across runs, so a rerun after an edit re-pays only what changed. Do not hoard runs or fear re-running: grow the play one stage at a time, rerun constantly, and let the cache carry the known-good prefix. This is also why EXPLORE is cheap: the parallel sweep runs candidate routes concurrently on a small sample, keeps the winner (shared/correctness.md), and only the loser's spend is ever wasted, once.
Runtime primitives, composition, authoring traps, and parallelism live in shared/authoring.md. Exact SDK signatures: https://deepline.com/docs/sdk-v2/sdk-reference. HTTP invocation: https://deepline.com/docs/sdk-v2/api-reference. The route-fanout skeleton — fan out, fuse (reciprocal-rank), judge, emit a per-route ledger — is plays/route-fanout.play.ts (shared/correctness.md § Fork the comparison-run harness); the coverage escalation ladder is jobs/enriching.md § Start cheap, measure coverage, escalate.
Read the matching doc before executing. The rules here apply to every task; the docs encode what previous runs learned the expensive way.
| If you're about to… | Read |
|---|---|
| Find companies and contacts (no rows yet) | jobs/finding.md |
| Fill columns on existing rows: emails, phones, signals, AI research | jobs/enriching.md |
| Write per-row outreach copy off research columns | jobs/writing.md |
Build, copy, customize, or debug a custom *.play.ts file | shared/authoring.md |
| Rank an uncertain route, or QA/verify a dataset before shipping | shared/correctness.md |
| Rank/judge a research shortlist (which sources best answer the question) | shared/reranking.md |
| Diagnose a run that failed, stalled, or produced wrong output | references/debugging.md |
| Look up exact SDK signatures or HTTP contracts | the two hosted doc URLs above |
| Exit: extract / convert a Clay table | route to clay-to-deepline |
Multi-phase tasks read the jobs docs in order (finding → enriching → writing) and shared/correctness.md before shipping.
Before EXPLORE spends, confirm the ground: deepline health, deepline auth status --json (not registered → deepline auth register), and for CSV tasks deepline csv show --csv rows.csv --summary. Discover the current name with deepline tools list <category> --json / deepline plays search <category> --json, confirm its contract with describe, then run: a prebuilt as prebuilt/<name>, a local file by path (deepline plays run <file.play.ts> while iterating; set-live when stable). Inspect with deepline runs get <id> --full --json, export with deepline runs export <run-id> --out <path>.
Run deepline commands directly and read the complete output. Piping human-formatted output into head/tail/grep/jq or backgrounding truncates the errors and run URLs you need; --json output is safe to pipe and parse, so use it whenever a downstream step parses output. plays run waits and streams completion by default (--watch is a compatibility alias); --no-wait starts the run and returns the id. Row totals live in rowCounts.{persisted, succeeded, failed} alongside dataset_execution_stats. The runtime already counted, so read those instead of hand-tallying.
Take the customer along. The session is the product. A run that goes silent until a final CSV is a black box even when correct. Announce the route map before spending, show real pilot rows the moment they exist, checkpoint each stage with a compact scoreboard ("12 emails: 6 valid, 6 verify_next" builds trust; a bare "12 emails" spends it), and deliver increments. Ask when ambiguity is load-bearing. A segment label, threshold, ICP boundary, or "which of these two people did you mean" can change the outcome, and one wrong full run costs more than one question; a verified answer becomes golden truth. Don't ask about things the pilot can answer.
The "why" matters more than the rule — knowing the failure mode lets you handle edge cases. (Pilot-before-scale, null-over-invention, and coverage/cost are LAWS above; these are the mechanics.)
name, title, company, email and got seniority, last_changed_at, source too? Keep all seven.ctx.* (replay-safety). Inside a *.play.ts, every non-deterministic operation goes through the runtime: ctx.tools.execute, ctx.runPlay, ctx.csv, ctx.dataset, ctx.fetch, ctx.log, ctx.sleep. The body re-executes during replay; process.env, Date.now(), fs, or raw fetch see different values on the second execution and corrupt the workflow.prebuilt/<name>. If it fits, run it; if only CSV headers differ, pass --columns.<field> "<Header>". Copy (deepline plays get <name> --source --out ./my-play.play.ts) only for semantic changes — the copy preserves the provider order the prebuilt already got right.-v2/-fixed/-final variants./tmp/ are wiped on reboot and users have lost paid enrichment outputs there; credit-costing outputs belong in a directory the user controls. Set up a task-descriptive slug at step zero: WORKDIR="deepline/data/<slug>" && mkdir -p "$WORKDIR". Keep custom *.play.ts files in the current workspace (where node_modules/deepline is available). After a run, export to the user-requested path with deepline runs export <run-id> --out <path> and report the exact path plus the run URL.input: { csv: string } pairs with --csv leads.csv and ctx.csv(input.csv, { columns, required }). The return value is a PlayDataset — pass it to ctx.dataset; use count()/peek() for bounded inspection. Row progress, retries, idempotency, and table output depend on the runtime owning the dataset.59a5bd2
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.