CtrlK
BlogDocsLog inGet started
Tessl Logo

deepline-plays

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

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

SKILL.md
Quality
Evals
Security

Deepline Plays

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

Deepline 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> --json lists 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"), whose describe may advertise a wrong input key. That error is a config artifact, not a real failure — re-run against the prebuilt/ ref.

When unsure about command shape, run deepline <command> --help before guessing flags.

The lifecycle: EXPLORE → SCORE → EXPLOIT → VERIFY

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?

Worked example: "Find 10 CTOs + work emails"

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 --watch

Pilot 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:

routecoverage (non_empty)correct vs truthcost/row
finder + validator waterfall3/3 (100%)3/3~4 cr
people-search aggregator2/3 (67%)2/2~6 cr
finder-only, no validator3/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 --watch

VERIFY. 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.

Laws (non-negotiable — these dominate every other rule below)

  1. Pilot before scale. Show real pilot rows and get an explicit go before any paid full run. A misshaped payload burns credits across hundreds of rows; a pilot exposes it on row 1 for cents.
  2. Take the human along — the session is the product. Announce the route map + expected cost before spending, show real rows within minutes, checkpoint each phase with a compact scoreboard (coverage / validation / credits / next), deliver increments. A run that goes silent until a final CSV is a black box even when it's correct.
  3. Null over invention. When tools come up empty, return null (or fewer rows). Never pattern-complete a name, email, company, or fact from training data — that ships unverifiable rows that look like success and fail at outreach time.
  4. Confirm before you spend. The live CLI is the source of truth for tool/play names and input shapes (tools describe, plays describe). Dynamic ctx.tools.execute refs are NOT preflight-checked — a wrong id fails only at run time.
  5. The judge is task-shaped. Relevance ranks which source to read; validation decides is this contact real (identity gate + validator); corroboration decides which number is true (agreement across independent sources). Never trust a reranked #1 as a verified fact.
  6. You are the judge, not deeplineagent. The agent plans and judges; the play is the deterministic executor. The model step (rerank) is a cheap subagent — never deeplineagent, never inside the durable play (shared/reranking.md).
  7. Every value travels with its evidence. Preserve provider evidence columns; deliver research findings with their source URL. A number without a source is a claim, not data.
  8. Only Deepline spend is customer-facing. Never surface provider USD. Never test on customer accounts, workspaces, keys, or credits — internal/test only.
  9. Coverage and cost are decisions you surface, not defaults you hide. Quote cost as marginal per unit (~X credits/email), never the pilot's run total — a small batch's total is dominated by fixed compute and expensive validators that amortize over scale, so "11 credits for 2 emails" misleads. And report coverage as a number the runtime already computed: read 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.

Field notes — failures to not repeat (from real runs, 2026-07-23)

  • Hand-authoring a play for a standard task is the tax. A CTO-email pilot ran 4.5 minutes because the default was a custom *.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).
  • A plain play name can shadow a stale copy. A run against 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.
  • A dead tool ref ships silently. A research route used 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).
  • A low fill rate is usually a credentials gap, not a data gap. Email/research recall collapsed because hunter/findymail/prospeo/fullenrich/exa were uncredentialed and short-circuited to 0ms. Check which routes actually executed before concluding coverage is poor.
  • A reranked #1 is not a true fact. A research run ranked a credible source top, but a dubious lone-source "$965B valuation" survived; the real answer ("$380B") held only because three independent sources corroborated it. Corroborate numbers; don't trust rank (LAW 5).
  • An all-null email column can mean the play never resolves emails. company-to-contact returns identity only (email: null always) — you must chain the email waterfall. Don't read all-null as "no emails found."
  • The trust gate trades recall for precision. The email identity gate withheld correct fused answers as verify_next while letting one wrong answer through. Unshipped ≠ wrong and shipped ≠ all-correct — read the tags, and re-check borderline rows before concluding.
  • Trust the runtime; a transient error is not a data miss. The runtime retries safe transient calls. If retries are exhausted, the provider failure stays loud unless the Play explicitly catches 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.

Surface lifecycle

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.

Execution philosophy: everything in the play, iteration is nearly free

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.

Choose your job

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 researchjobs/enriching.md
Write per-row outreach copy off research columnsjobs/writing.md
Build, copy, customize, or debug a custom *.play.ts fileshared/authoring.md
Rank an uncertain route, or QA/verify a dataset before shippingshared/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 outputreferences/debugging.md
Look up exact SDK signatures or HTTP contractsthe two hosted doc URLs above
Exit: extract / convert a Clay tableroute to clay-to-deepline

Multi-phase tasks read the jobs docs in order (finding → enriching → writing) and shared/correctness.md before shipping.

CLI mechanics that back the lifecycle

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.

Cross-cutting rules

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.)

  • Over-provision, then filter. For N rows, pull ~1.4×N at the top of funnel. Every phase has natural falloff, and companies providers can't find contacts for are the same ones without email coverage — coverage is a property of the company, not something retries overcome. Deliver the best N complete rows; let incomplete rows fall off.
  • Preserve provider evidence columns. Responses include proof fields the user did not ask for: source URL, funding metadata, validation status, confidence, hiring counts, provenance. Verification runs on evidence, so trimming to display columns destroys the ability to trust the dataset. Asked for name, title, company, email and got seniority, last_changed_at, source too? Keep all seven.
  • All I/O through 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.
  • Prebuilts are templates. Search first, reference as 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.
  • One run per file while iterating. Edit one local play file in place; the durable cache makes reruns cheap when names and keys stay stable. No -v2/-fixed/-final variants.
  • Outputs go in a project-local working directory. Files in /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.
  • CSV inputs are runtime datasets. 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.
Repository
getaero-io/gtm-eng-skills
Last updated
First committed

Is this your skill?

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.