.recipe-template
# Recipe Template
Use this structure for all recipes in `recipes/`.
```markdown
---
name: recipe-name
description: "One-line description of what this recipe does."
---
# Recipe Title
One-line summary of the workflow.
## When to use
- "Example user request that triggers this recipe"
- "Another example"
## What NOT to do
| Anti-pattern | What happens | Why it fails |
|-------------|-------------|-------------|
| Describe the wrong approach | What the agent will observe | Root cause |
## Steps
### Step 1: [verb] — [category: search|enrich|transform|compose|deliver]
Description of what this step does.
**Input:** What data/fields this step needs and where they come from.
```bash
# exact command
```
**Output:** What this step produces — columns, format, expected row count.
**Checkpoint:** Validation gate before proceeding (e.g., "verify row count matches expected").
**Fallback:** Alternative approach if the primary fails.
### Step 2: ...
(repeat for each step)
## Gotchas
| Gotcha | What happens | Fix |
|--------|-------------|-----|
| Known issue | Observable symptom | Workaround or prevention |
```
## Step categories
| Category | Covers |
|----------|--------|
| **search** | Finding/pulling data from providers — scraping, discovery, datasets |
| **enrich** | Adding data to existing rows — qualification, email lookup, profiles |
| **transform** | Reshaping data — filtering, merging, deduping, formatting |
| **compose** | Generating content — emails, personalization, scoring narratives |
| **deliver** | Pushing results to a destination — Sheets, CRM, sequencer, CSV export |
## Principles
- Every step must declare its **input** (what fields/format it needs) and **output** (what it produces).
- Every step should have a **checkpoint** before the next step consumes its output.
- Every step should have a **fallback** — an alternative tool or approach if the primary fails.
- Anti-patterns in "What NOT to do" should come from real session failures, not speculation.
agents/execution-plan-creator.md
---
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.
agents/list-builder.md
---
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.
claude-deepline-statusline.mjs
#!/usr/bin/env node
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execSync } from 'node:child_process';
const MAX_TRANSCRIPT_BYTES = 1024 * 1024;
const MAX_RUNNING_SHOWN = 2;
const BACKEND_STATUS_TTL_MS = 15000;
const ACTIVE_PERSIST_MS = 45_000;
const STATE_PATH = path.join(
os.homedir(),
'.claude',
'deepline-statusline-state.json',
);
const USER_CMD_PATH = path.join(
os.homedir(),
'.claude',
'statusline-user-command.txt',
);
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const BACKEND_PULSE = ['●', '◉'];
const C = {
reset: '\x1b[0m',
bold: '\x1b[1m',
white: '\x1b[97m',
dim: '\x1b[37m',
cyan: '\x1b[96m',
green: '\x1b[92m',
red: '\x1b[91m',
yellow: '\x1b[93m',
};
const PROVIDER_LABELS = {
apify: 'Apify',
crustdata: 'CrustData',
deepline_native: 'Deepline Native',
dropleads: 'DropLeads',
exa: 'Exa',
leadmagic: 'LeadMagic',
hunter: 'Hunter',
parallel: 'Parallel',
peopledatalabs: 'People Data Labs',
adyntel: 'Adyntel',
google_search: 'Google Search',
instantly: 'Instantly',
lemlist: 'Lemlist',
heyreach: 'HeyReach',
};
function ansi(text, color, useBold = false) {
return `${useBold ? C.bold : ''}${color}${text}${C.reset}`;
}
function stripAnsi(text) {
return text.replace(/\x1b\[[0-9;]*m/g, '');
}
function width() {
const w = Number(process.stdout.columns || process.env.COLUMNS || 120);
return Number.isFinite(w) && w > 20 ? w : 120;
}
function truncateText(text, maxLen) {
if (maxLen <= 0) return '';
if (text.length <= maxLen) return text;
if (maxLen <= 3) return '.'.repeat(maxLen);
return `${text.slice(0, maxLen - 3)}...`;
}
function joinInline(left, right) {
const w = width();
const leftRaw = stripAnsi(left);
const sepRaw = ' | ';
const leftLen = leftRaw.length;
const sepLen = sepRaw.length;
const availableRight = Math.max(16, w - leftLen - sepLen);
const trimmedRight = truncateText(stripAnsi(right), availableRight);
return `${left}${ansi(sepRaw, C.dim)}${ansi(trimmedRight, C.white, true)}`;
}
async function readStdin() {
if (process.stdin.isTTY) return null;
const chunks = [];
process.stdin.setEncoding('utf8');
try {
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const text = chunks.join('').trim();
return text.length ? text : null;
} catch {
return null;
}
}
function parseJsonLine(line) {
try {
return JSON.parse(line);
} catch {
return null;
}
}
function readTranscriptTail(transcriptPath, maxBytes = MAX_TRANSCRIPT_BYTES) {
if (!transcriptPath) return [];
try {
const resolved = path.resolve(transcriptPath);
const stat = fs.statSync(resolved);
if (!stat.isFile()) return [];
const start = Math.max(0, stat.size - maxBytes);
const len = stat.size - start;
const fd = fs.openSync(resolved, 'r');
const buffer = Buffer.alloc(len);
fs.readSync(fd, buffer, 0, len, start);
fs.closeSync(fd);
let text = buffer.toString('utf8');
if (start > 0) {
const firstNl = text.indexOf('\n');
if (firstNl >= 0) text = text.slice(firstNl + 1);
}
return text
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map(parseJsonLine)
.filter(Boolean);
} catch {
return [];
}
}
function getToolUsesFromMessage(message) {
const content = Array.isArray(message?.content) ? message.content : [];
return content.filter((b) => b?.type === 'tool_use');
}
function getToolResultsFromMessage(message) {
const content = Array.isArray(message?.content) ? message.content : [];
return content.filter((b) => b?.type === 'tool_result');
}
function extractToolUseCommand(toolUse) {
const input = toolUse?.input;
if (!input || typeof input !== 'object') return '';
if (typeof input.command === 'string') return input.command;
if (typeof input.cmd === 'string') return input.cmd;
return '';
}
function extractToolUseName(toolUse) {
const name = toolUse?.name;
return typeof name === 'string' ? name : '';
}
function extractThinkingText(lines) {
for (let i = lines.length - 1; i >= 0; i -= 1) {
const message = lines[i]?.message;
const content = Array.isArray(message?.content) ? message.content : [];
for (let j = content.length - 1; j >= 0; j -= 1) {
const block = content[j];
if (block?.type === 'text' && typeof block?.text === 'string') {
const text = block.text.trim();
if (text) return text;
}
}
}
return '';
}
function isDeeplineCommand(command) {
return /(^|\s)(deepline|dl)(\s|$)/.test(command);
}
function isDeeplineToolUse(toolUse) {
const name = String(toolUse?.name || '').toLowerCase();
if (name.includes('deepline')) return true;
const cmd = extractToolUseCommand(toolUse);
return Boolean(cmd && isDeeplineCommand(cmd));
}
function classifyCommand(command) {
if (!command) return 'running';
if (/\b(deepline|dl)\s+enrich\b/.test(command)) return 'enrich';
if (/\b(deepline|dl)\s+tools\s+describe\b/.test(command))
return 'tools_describe';
if (/\b(deepline|dl)\s+tools\s+execute\b/.test(command))
return 'tools_execute';
if (/\b(deepline|dl)\s+tools\s+(search|list)\b/.test(command))
return 'tools_search';
if (/\b(deepline|dl)\s+csv\s+(show|render)\b/.test(command)) return 'csv';
return 'running';
}
function isTrackableMode(mode) {
return (
mode === 'enrich' ||
mode === 'tools_describe' ||
mode === 'tools_execute' ||
mode === 'tools_search' ||
mode === 'csv' ||
mode === 'running'
);
}
function shouldUseCachedBackendOnly(command) {
if (!command) return false;
return (
/\b(deepline|dl)\s+csv\s+render\b/.test(command) ||
/\b(deepline|dl)\s+enrich\b/.test(command)
);
}
function parseRowCount(command) {
const rows = command.match(/--rows\s+(\d+)\s*:\s*(\d+)/);
if (rows) {
const start = Number(rows[1]);
const end = Number(rows[2]);
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
return end - start + 1;
}
}
const limit = command.match(/--limit\s+(\d+)/);
if (limit) {
const n = Number(limit[1]);
if (Number.isFinite(n) && n > 0) return n;
}
return null;
}
function extractCsvPath(command) {
const patterns = [
/--input\s+("([^"]+)"|'([^']+)'|(\S+))/,
/--csv\s+("([^"]+)"|'([^']+)'|(\S+))/,
/--output\s+("([^"]+)"|'([^']+)'|(\S+))/,
];
for (const p of patterns) {
const m = command.match(p);
if (m) return m[2] || m[3] || m[4] || '';
}
return '';
}
function extractExecutedTools(command) {
const out = [];
const execRe = /(?:deepline|dl)\s+tools\s+execute\s+([a-zA-Z0-9_:-]+)/g;
let m = execRe.exec(command);
while (m) {
out.push(m[1]);
m = execRe.exec(command);
}
const withRe = /=[\s"']*([a-zA-Z0-9_]+):\{/g;
m = withRe.exec(command);
while (m) {
out.push(m[1]);
m = withRe.exec(command);
}
return [...new Set(out)];
}
function extractToolsDescribeTarget(command) {
const m = command.match(
/(?:deepline|dl)\s+tools\s+describe\s+([a-zA-Z0-9_:-]+)/,
);
return m ? m[1] : '';
}
function extractPayloadPreview(command) {
const idx = command.indexOf('--payload');
if (idx < 0) return '';
let raw = command.slice(idx + '--payload'.length).trim();
if (!raw) return '';
const nextFlag = raw.search(/\s--[a-zA-Z0-9_-]+/);
if (nextFlag > 0) raw = raw.slice(0, nextFlag).trim();
if (
(raw.startsWith('"') && raw.endsWith('"')) ||
(raw.startsWith("'") && raw.endsWith("'"))
) {
raw = raw.slice(1, -1).trim();
}
return truncateText(raw.replace(/\s+/g, ' '), 18);
}
function extractPayloadQuery(command) {
const idx = command.indexOf('--payload');
if (idx < 0) return '';
let raw = command.slice(idx + '--payload'.length).trim();
if (!raw) return '';
const nextFlag = raw.search(/\s--[a-zA-Z0-9_-]+/);
if (nextFlag > 0) raw = raw.slice(0, nextFlag).trim();
if (
(raw.startsWith('"') && raw.endsWith('"')) ||
(raw.startsWith("'") && raw.endsWith("'"))
) {
raw = raw.slice(1, -1).trim();
}
try {
const parsed = JSON.parse(raw);
if (
parsed &&
typeof parsed === 'object' &&
typeof parsed.query === 'string'
) {
return truncateText(parsed.query.replace(/\s+/g, ' ').trim(), 48);
}
} catch {
// fall through to regex extraction
}
const m = raw.match(/"query"\s*:\s*"([^"]+)"/);
if (m && m[1]) {
return truncateText(m[1].replace(/\s+/g, ' ').trim(), 48);
}
return '';
}
function getProviderLabel(toolId) {
const prefixes = Object.keys(PROVIDER_LABELS).sort(
(a, b) => b.length - a.length,
);
for (const prefix of prefixes) {
if (toolId === prefix || toolId.startsWith(`${prefix}_`))
return PROVIDER_LABELS[prefix];
}
return null;
}
function titleCaseWords(text) {
return text
.split(/[_\-\s]+/)
.filter(Boolean)
.map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase())
.join(' ');
}
function labelAction(actionRaw) {
const action = actionRaw.toLowerCase();
if (action.includes('linkedin') && action.includes('post'))
return 'LinkedIn Posts';
if (action.includes('job') || action.includes('hiring'))
return 'Job Listings';
if (action.includes('comment') && action.includes('filter'))
return 'Comment Filtering';
if (action.includes('comment') && action.includes('search'))
return 'Comment Search';
if (action.includes('tam')) return 'TAM Analysis';
if (action.includes('people') && action.includes('search'))
return 'People Search';
if (action.includes('company') && action.includes('search'))
return 'Company Search';
if (
action.includes('email') &&
(action.includes('finder') || action.includes('find'))
)
return 'Email Finder';
if (action.includes('verify') || action.includes('validation'))
return 'Verification';
return null;
}
function formatToolName(toolId) {
const provider = getProviderLabel(toolId);
const prefixes = Object.keys(PROVIDER_LABELS).sort(
(a, b) => b.length - a.length,
);
if (provider) {
const prefix = prefixes.find(
(p) => toolId === p || toolId.startsWith(`${p}_`),
);
const actionRaw = prefix
? toolId.slice(prefix.length).replace(/^_+/, '')
: toolId;
if (!actionRaw) return provider;
const clean = actionRaw
.replace(/_search$/, ' search')
.replace(/_finder$/, ' finder')
.replace(/_enrichment$/, ' enrichment')
.replace(/_verify|_validation$/, ' verify');
return `${provider}: ${labelAction(clean) || titleCaseWords(clean)}`;
}
return labelAction(toolId) || titleCaseWords(toolId);
}
function shortToolTarget(toolId) {
if (!toolId) return 'tool';
const provider = getProviderLabel(toolId);
if (provider) {
return provider.toLowerCase();
}
return toolId.split('_')[0].toLowerCase();
}
function summarizeProviders(toolIds) {
const names = [
...new Set(toolIds.map((id) => getProviderLabel(id)).filter(Boolean)),
];
return names.join(', ');
}
function basenameSafe(filePath) {
return filePath ? path.basename(filePath) : '';
}
function explainCommand(command) {
const mode = classifyCommand(command);
const toolIds = extractExecutedTools(command);
const primaryTool = toolIds[0] || '';
const target = shortToolTarget(primaryTool);
const primaryLabel = primaryTool ? formatToolName(primaryTool) : '';
const rows = parseRowCount(command);
const csv = basenameSafe(extractCsvPath(command));
const payload = extractPayloadPreview(command);
const payloadQuery = extractPayloadQuery(command);
if (mode === 'enrich') {
if (rows && csv)
return {
detail: `Enriching ${rows} rows (${csv})`,
current: '',
providers: '',
};
if (rows)
return { detail: `Enriching ${rows} rows`, current: '', providers: '' };
return { detail: 'Enriching rows', current: '', providers: '' };
}
if (mode === 'tools_describe') {
const target = extractToolsDescribeTarget(command);
return {
detail: `Learning ${shortToolTarget(target)}`,
current: '',
providers: '',
};
}
if (mode === 'tools_execute') {
if (payloadQuery && primaryLabel)
return {
detail: `${primaryLabel}: ${payloadQuery}`,
current: '',
providers: '',
};
if (primaryLabel && payload)
return {
detail: `${primaryLabel}: ${payload}`,
current: '',
providers: '',
};
if (primaryLabel)
return { detail: `Running ${primaryLabel}`, current: '', providers: '' };
if (payload)
return {
detail: `Calling ${target} with ${payload}`,
current: '',
providers: '',
};
return { detail: `Calling ${target}`, current: '', providers: '' };
}
if (mode === 'tools_search') {
return { detail: 'Searching tools', current: '', providers: '' };
}
if (mode === 'csv') {
return {
detail: rows ? `Running CSV (${rows} rows)` : 'Running CSV',
current: '',
providers: '',
};
}
return { detail: '', current: '', providers: '' };
}
function loadState() {
try {
const raw = fs.readFileSync(STATE_PATH, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object')
return { sessions: {}, backend: {} };
return {
sessions:
parsed.sessions && typeof parsed.sessions === 'object'
? parsed.sessions
: {},
backend:
parsed.backend && typeof parsed.backend === 'object'
? parsed.backend
: {},
};
} catch {
return { sessions: {}, backend: {} };
}
}
function saveState(state) {
try {
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
} catch {
// Keep status line resilient.
}
}
function nextSpinnerFrame(sessionState) {
const index = Number.isFinite(sessionState?.spinner_index)
? sessionState.spinner_index
: 0;
const next = (index + 1) % SPINNER_FRAMES.length;
sessionState.spinner_index = next;
return SPINNER_FRAMES[next];
}
function parseBackendUpFromText(text) {
if (!text) return { up: null, renderUrl: '' };
const lines = text.split('\n');
let section = '';
let backendStatus = null;
let renderStatus = null;
let renderUrl = '';
let backendApiUrl = '';
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
if (/^backend:/i.test(line)) {
section = 'backend';
continue;
}
if (/^render:/i.test(line)) {
section = 'render';
continue;
}
if (
/^[a-z][a-z0-9 _-]*:/i.test(line) &&
!/^status:/i.test(line) &&
!/^api url:/i.test(line) &&
!/^url:/i.test(line)
) {
section = '';
}
const statusMatch = line.match(/^status:\s*([a-z_ -]+)/i);
if (statusMatch) {
const val = statusMatch[1].trim().toLowerCase();
if (section === 'backend') backendStatus = val;
if (section === 'render') renderStatus = val;
}
const apiUrlMatch = line.match(/^api url:\s*(https?:\/\/\S+)/i);
if (apiUrlMatch && section === 'backend') {
backendApiUrl = apiUrlMatch[1];
}
const renderUrlMatch = line.match(/^url:\s*(https?:\/\/\S+)/i);
if (renderUrlMatch && section === 'render') {
renderUrl = renderUrlMatch[1];
}
}
const lower = text.toLowerCase();
if (!backendStatus) {
if (
lower.includes('"backend":{"running":true') ||
lower.includes('"backend":{"status":"running"')
) {
backendStatus = 'running';
} else if (
lower.includes('"backend":{"running":false') ||
lower.includes('"backend":{"status":"stopped"')
) {
backendStatus = 'stopped';
}
}
let up = null;
if (backendStatus) {
up = backendStatus.includes('running') || backendStatus.includes('healthy');
} else if (lower.includes('backend') && lower.includes('running')) {
up = true;
} else if (
lower.includes('backend') &&
(lower.includes('stopped') ||
lower.includes('not running') ||
lower.includes('down'))
) {
up = false;
}
if (
!renderUrl &&
renderStatus &&
renderStatus.includes('running') &&
backendApiUrl
) {
renderUrl = backendApiUrl;
}
return { up, renderUrl };
}
function parseBackendUpFromJson(output) {
try {
const parsed = JSON.parse(output);
let up = null;
if (typeof parsed?.backend?.running === 'boolean') {
up = parsed.backend.running;
} else if (typeof parsed?.backend?.healthy === 'boolean') {
up = parsed.backend.healthy;
} else if (typeof parsed?.running === 'boolean') {
up = parsed.running;
} else if (typeof parsed?.healthy === 'boolean') {
up = parsed.healthy;
} else if (typeof parsed?.status === 'string') {
const s = parsed.status.toLowerCase();
up = s === 'ok' || s.includes('running') || s.includes('healthy');
} else if (typeof parsed?.backend?.status === 'string') {
const s = parsed.backend.status.toLowerCase();
up = s.includes('running') || s.includes('healthy');
}
const renderStatus = String(
parsed?.render?.status || parsed?.render_status || '',
).toLowerCase();
let renderUrl = String(
parsed?.render?.url || parsed?.render_url || parsed?.csv_render_url || '',
).trim();
if (!renderUrl && renderStatus.includes('running')) {
renderUrl = String(
parsed?.backend?.api_url || parsed?.api_url || '',
).trim();
}
return { up, renderUrl };
} catch {
return parseBackendUpFromText(output);
}
}
function getBackendStatus(state, opts = {}) {
const { allowProbe = true } = opts;
const now = Date.now();
const cached = state.backend || {};
if (
Number.isFinite(cached.checked_at_ms) &&
now - cached.checked_at_ms < BACKEND_STATUS_TTL_MS &&
typeof cached.up === 'boolean'
) {
return {
up: cached.up,
renderUrl: cached.renderUrl || '',
checked_at_ms: cached.checked_at_ms,
};
}
if (!allowProbe) {
return {
up: typeof cached.up === 'boolean' ? cached.up : null,
renderUrl: cached.renderUrl || '',
checked_at_ms: Number.isFinite(cached.checked_at_ms)
? cached.checked_at_ms
: 0,
};
}
let up = null;
let renderUrl = '';
try {
const out = execSync('deepline health --json', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 1200,
});
const parsed = parseBackendUpFromJson(String(out || ''));
up = parsed.up;
renderUrl = parsed.renderUrl || '';
} catch (err) {
const stdout = String(err?.stdout || '');
const stderr = String(err?.stderr || '');
const parsed = parseBackendUpFromText(`${stdout}\n${stderr}`);
up = typeof parsed.up === 'boolean' ? parsed.up : false;
renderUrl = parsed.renderUrl || '';
}
const status = { up, renderUrl, checked_at_ms: now };
state.backend = status;
return status;
}
function buildActiveFromTranscript(lines, fallbackCommand) {
const pending = new Map();
const deeplineHistory = [];
for (const entry of lines) {
if (entry?.type === 'assistant') {
for (const toolUse of getToolUsesFromMessage(entry.message)) {
pending.set(toolUse.id, toolUse);
if (isDeeplineToolUse(toolUse)) deeplineHistory.push(toolUse);
}
continue;
}
if (entry?.type === 'user') {
for (const result of getToolResultsFromMessage(entry.message)) {
if (typeof result?.tool_use_id === 'string')
pending.delete(result.tool_use_id);
}
}
}
const active = [...pending.values()].filter((toolUse) =>
isDeeplineToolUse(toolUse),
);
if (active.length > 0) {
const latestCmd =
extractToolUseCommand(active[active.length - 1]) || fallbackCommand || '';
if (!isTrackableMode(classifyCommand(latestCmd))) return null;
const explained = explainCommand(latestCmd);
const runningIds = [
...new Set(
active.flatMap((toolUse) =>
extractExecutedTools(extractToolUseCommand(toolUse)),
),
),
];
const currentFallback = runningIds
.slice(0, MAX_RUNNING_SHOWN)
.map((id) => formatToolName(id))
.join(', ');
return {
detail: explained.detail,
current: explained.current || currentFallback,
providers: explained.providers || summarizeProviders(runningIds),
summary:
explained.current || explained.detail || currentFallback || 'workflow',
};
}
const fallback =
fallbackCommand ||
extractToolUseCommand(deeplineHistory[deeplineHistory.length - 1]) ||
'';
if (!fallback || !isDeeplineCommand(fallback)) return null;
if (!isTrackableMode(classifyCommand(fallback))) return null;
if (deeplineHistory.length === 0) {
const explained = explainCommand(fallback);
return {
detail: explained.detail,
current: explained.current,
providers: explained.providers,
summary:
explained.current ||
explained.detail ||
explained.providers ||
'workflow',
};
}
return null;
}
function backendBadge(backend, frameIndex) {
if (typeof backend?.up !== 'boolean') {
return '';
}
void frameIndex;
if (backend.up) {
return ansi('✅ Deepline reachable', C.green, true);
}
return ansi('⏸️ Deepline unreachable', C.red, true);
}
function renderActiveLine(frame, status, backend, frameIndex, thinking) {
const detail = status.detail || 'Running task';
const thinkingSuffix = thinking ? ` | ${thinking}` : '';
const rightRaw = `${frame} ${detail}${thinkingSuffix}`;
const right = ansi(rightRaw, C.white, true);
const left = backendBadge(backend, frameIndex);
if (!left) return right;
return joinInline(left, right);
}
function runUserStatusline(input) {
try {
const cmd = fs.readFileSync(USER_CMD_PATH, 'utf8').trim();
if (!cmd) return '';
const out = execSync(cmd, {
input,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 2000,
});
return (out || '').trim();
} catch {
return '';
}
}
async function main() {
const input = await readStdin();
if (!input) return;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
const fallbackCommand = typeof data?.command === 'string' ? data.command : '';
const transcriptPath =
typeof data?.transcript_path === 'string' ? data.transcript_path : '';
const sessionId =
typeof data?.session_id === 'string' ? data.session_id : 'default';
const state = loadState();
const sessionState = state.sessions[sessionId] || {
spinner_index: 0,
was_active: false,
active_summary: '',
last_ran: '',
};
const frame = nextSpinnerFrame(sessionState);
const frameIndex = sessionState.spinner_index || 0;
const lines = readTranscriptTail(transcriptPath);
const activeStatus = buildActiveFromTranscript(lines, fallbackCommand);
const latestThinking = extractThinkingText(lines);
const backendStatus = getBackendStatus(state, {
allowProbe: !activeStatus && !shouldUseCachedBackendOnly(fallbackCommand),
});
// Compute deepline-specific output
let dlOutput = '';
if (activeStatus) {
sessionState.was_active = true;
sessionState.active_summary = activeStatus.summary || activeStatus.detail;
sessionState.active_started_ms = Date.now();
state.sessions[sessionId] = sessionState;
saveState(state);
const activeThinking = latestThinking
? `Thinking: ${truncateText(latestThinking, 56)}`
: '';
dlOutput = renderActiveLine(
frame,
activeStatus,
backendStatus,
frameIndex,
activeThinking,
);
} else {
const now = Date.now();
const lastActiveMs = Number.isFinite(sessionState.active_started_ms)
? sessionState.active_started_ms
: 0;
if (
sessionState.was_active &&
lastActiveMs &&
now - lastActiveMs < ACTIVE_PERSIST_MS &&
sessionState.active_summary
) {
sessionState.was_active = true;
state.sessions[sessionId] = sessionState;
saveState(state);
const persisted = {
detail: sessionState.active_summary,
current: sessionState.active_summary,
providers: '',
summary: sessionState.active_summary,
};
const activeThinking = latestThinking
? `Thinking: ${truncateText(latestThinking, 56)}`
: '';
dlOutput = renderActiveLine(
frame,
persisted,
backendStatus,
frameIndex,
activeThinking,
);
} else {
if (sessionState.was_active && sessionState.active_summary) {
sessionState.last_ran = sessionState.active_summary;
}
sessionState.was_active = false;
sessionState.active_summary = '';
state.sessions[sessionId] = sessionState;
saveState(state);
}
}
// Chain user's own statusline (preserved during install)
const userOutput = runUserStatusline(input);
// Line 1: user's personal statusline
if (userOutput) console.log(userOutput);
// Line 2: deepline status (only when active)
if (dlOutput) console.log(dlOutput);
}
main();
enriching-and-researching.md
# 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>` and `deepline plays list
--show-cost`; read contracts with `deepline plays describe <name>`. 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_persondb_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
# 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_companydb_search`** — adds investors, funding stage, fuzzy `(.)` operator. Use `crustdata_companydb_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_companydb_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_persondb_search`** — cheapest bulk paid. Use `crustdata_persondb_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_companydb_search --payload '{"filters":[{"filter_type":"crunchbase_categories","type":"in","value":["Identity Management","Fraud Detection"]},{"filter_type":"hq_country","type":"=","value":"USA"},{"filter_type":"employee_count_range","type":"in","value":["51-200","201-500"]},{"filter_type":"last_funding_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_v2_job_search` in the Hiring-qualified section below.
```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_companydb_autocomplete --payload '{"field":"crunchbase_categories","query":"identity","limit":5}'
```
### Count-first structured company search
```bash
deepline tools execute crustdata_companydb_search --payload '{"filters":[{"filter_type":"crunchbase_categories","type":"in","value":["Identity Management","Fraud Detection"]},{"filter_type":"hq_country","type":"=","value":"USA"},{"filter_type":"employee_count_range","type":"in","value":["51-200","201-500"]},{"filter_type":"last_funding_round_type","type":"in","value":["Series A","Series B"]}],"limit":1}'
```
### Full structured company pull
```bash
deepline tools execute crustdata_companydb_search --payload '{"filters":[{"filter_type":"crunchbase_categories","type":"in","value":["Identity Management","Fraud Detection"]},{"filter_type":"hq_country","type":"=","value":"USA"},{"filter_type":"employee_count_range","type":"in","value":["51-200","201-500"]},{"filter_type":"last_funding_round_type","type":"in","value":["Series A","Series B"]}],"sorts":[{"column":"employee_metrics.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_v2_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_companydb_search` | Company lists with ICP constraints (funding, headcount, geography, industry) | funding stage, headcount range, hq_country, crunchbase_categories, linkedin_industries, employee growth, investor | ~1 cr/search | `hq_country` = ISO 3-letter codes. Use `crunchbase_categories` for niche verticals. Response already includes headcount, funding, HQ, categories, and growth; don't re-enrich those. |
| `crustdata_companydb_autocomplete` | Get canonical filter values before searching | — | free | Run before `companydb_search` for categorical fields. Requires non-empty `query`. |
| `crustdata_v2_job_search` | Indexed job-listing discovery and hiring signals | company fields, title, location, job metadata | dynamic | Use company-domain or company-id filters. Coverage is thinner on smaller companies; use `employee_metrics.growth_6m_percent` first when available. |
| `crustdata_people_search` | LinkedIn-oriented person discovery | company domain, title keywords | ~1 cr | — |
| `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_research` | Deep multi-source synthesis | outputSchema, multi-query | ~10 cr | 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_person_enrichment` | LinkedIn profile enrichment | LinkedIn URL | ~1 cr | — |
| `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 `src/lib/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_companydb_autocomplete --payload '{"field":"crunchbase_categories","query":"fraud","limit":5}'
deepline tools execute crustdata_companydb_autocomplete --payload '{"field":"linkedin_industries","query":"financial","limit":5}'
```
```bash
# Use crunchbase_categories for niche verticals, hq_country with ISO codes
deepline tools execute crustdata_companydb_search --payload '{"filters":[{"filter_type":"crunchbase_categories","type":"in","value":["Fraud Detection","Identity Management"]},{"filter_type":"hq_country","type":"=","value":"USA"},{"filter_type":"employee_count_range","type":"in","value":["51-200","201-500"]},{"filter_type":"last_funding_round_type","type":"in","value":["Series A","Series B"]}],"sorts":[{"column":"employee_metrics.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_research` (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_people_search` | `deepline tools execute crustdata_people_search --payload '{"companyDomain":"notion.so","titleKeywords":["VP","Head"],"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 `niche-signal-discovery` skill if you have won/lost data. Otherwise enrich with `crustdata_v2_job_search` (hiring), `exa_search` w/ `includeDomains`+`contents` (website/pain language), then score.
prompts.json
{
"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 \\\"company_domain\\\": \\\"microsoft.com\\\"\\n }\\n ],\\n \\\"parent_hierarchy\\\": [],\\n \\\"subsidiary_hierarchy\\\": [\\n {\\n \\\"hq_territory\\\": \\\"Dublin, Ireland\\\",\\n \\\"legal_name\\\": \\\"Microsoft Ireland Operations Limited\\\",\\n \\\"company_domain\\\": \\\"microsoft.com\\\"\\n },\\n {\\n \\\"hq_territory\\\": \\\"Redmond, Washington, United States\\\",\\n \\\"legal_name\\\": \\\"LinkedIn Corporation\\\",\\n \\\"company_domain\\\": \\\"linkedin.com\\\"\\n },\\n {\\n \\\"hq_territory\\\": \\\"San Francisco, California, United States\\\",\\n \\\"legal_name\\\": \\\"GitHub Inc.\\\",\\n \\\"company_domain\\\": \\\"github.com\\\"\\n }\\n ],\\n \\\"confidence_level\\\": \\\"high\\\",\\n \\\"reasoning\\\": \\\"Microsoft Corporation identified as ultimate parent (L1) with no parent companies. Direct subsidiaries include major acquisitions LinkedIn and GitHub, plus regional operating entity Microsoft Ireland Operations Limited.\\\"\\n}\\n```\\n\\n## Instructions for Analysis\\n1. **Parse all data sources** - Extract entities and relationships from all inputs\\n2. **Build complete entity map** - Create master list of all unique legal entities\\n3. **Identify input company** - Match input against entity map\\n4. **Trace upward hierarchy** - Follow immediate_parent chain to ultimate parent\\n5. **Find immediate subsidiaries** - Identify ONLY entities with input company as immediate_parent\\n6. **Validate relationships** - Ensure no circular references or impossible structures\\n7. **Enrich with metadata** - Add domains, territories, and tax IDs where available\\n8. **Structure output arrays** - Separate into input, parent, and subsidiary arrays\\n9. **Assess confidence** - Based on data completeness and consistency\\n10. **Document analysis** - Explain key findings and decisions in reasoning\\n**Remember**: The subsidiary_hierarchy must contain ONLY immediate children of the input company, not the entire downstream corporate tree.\""
],
"Create JSON lists from a comma separated string": [
"\"I have a list of Names, Titles, and Universities in a comma separated list. I want to create 3 separate lists, one for just names, one for just titles, and one for just universities If there is no university present return \\\"No univ\\\" in the JSON object Return only the 3 separate lists and nothing else Here is the list: \" + {{input 1}}"
],
"Creative Prospecting Ideas": [
"\"# BACKGROUND #\\nUsing the input I'm giving you below, which is a combination of a scrape of the company website and additional information on their ICP, target industries, target personas, and value proposition, make a determination of how my company, , can help them pull data points for their prospecting. \\n\\n# INPUTS #\\nThese are the inputs to review: \\n\\nCompany Webpage Scrape: \"+{{input 1: bodyText}}+\"\\nIndustries of Operation: \"+{{input 2: Operating_Industries}}+\"\\nValue Prop: \"+{{input 3: Value_Proposition}}+\"\\nTarget Personas: \"+{{input 4: Target_Personas}}+\"\\nCore ICP: \"+{{input 5: Company_ICP}}+\"\\n\\n# CONTEXT #\\nHere is more information about my company: the platform is a workflow tool for sales and marketing teams that helps them pull custom data from over 150+ integration providers, as well as really custom data from their web scraping tool called . It can do research on any public data and is very good for getting really niche data sets.We are going to be creating ideas of how we can help for people we want to keep all of those ideas within the boundaries of broad things that can do for everyone but are also very highly requested (like pulling new hires of ICP contacts and companies, open jobs technology, who their competitors are, what their pricing is, what the prospects case studies are, and other niche data points that we're looking for).\\nI want you to think for as long as you need and think critically about how the customer in the input could use to pull in niche data sets for themselves to enrich their CRM, their outbound prospecting lists, or their inbound prospecting lists.\\nAll the ideas must be possible with and must reference publicly available data or something from one of our integration providers. Think critically about what data is useful for this company and think critically about the data that they wish that their reps would be able to manually research for 10 minutes about each company and each prospect, but that they would love to automate.\\nOutput bullet points of the creative enrichment ideas on separate lines.\\n\\nBullet point 1 should be about a niche creative data point that a company wishes they could prospect for but it takes time. For example, Rippling would research which companies have US based hires AND International hires so that they can tailor the message towards managing employees that are foreign and domestic. This should be tailed to the most relevant industry that \"+{{input 6: name}}+\" targets (see top industries here: \"+{{input 2: Operating_Industries}}+\" and reference these) and it should be very pertinent to their Core ICP, mentioned here: \"+{{input 5: Company_ICP}}+\". Think critically about what research points are publicly available that teams would want to automate.\\n\\nBullet point 2 should be about enriching their target accounts for their ideal contacts that are new in their role. An output could look something like \\\"Enrich your list for *insert ideal company types* that recently hired a new *insert ICP job title* and reach out as soon as they are detected. Refer to \"+{{input 4: Target_Personas}}+\" for ICP roles that \"+{{input 7: Company}}+\" consistently targets. \\nNOTE: \\\"*insert ideal company type*\\\" is generated by you after thinking about what kind of companies they want to sell to. \\\"*insert ICP job title*\\\" is generated by thinking about who they would want to sell to as a contact at that company.\\n\\nBullet point 3 should be about enriching their target accounts for recent news that would be relevant. A merger, new product, pricing change, fundraising announcement, partnership and more. You should get creative here and really push the boundaries of what can find for \"+{{input 7: Company}}+\" it should be signals or events that are highly pertinent to \"+{{input 7: Company}}+\", but may not be something they've ever thought of before.\\n\\nThink critically about what newsworthy events would be important for a company to reach out based on that news event being true.Keep the bullet points to the point and use my previous examples as guides of other things can do.\\n\\n# OUTPUT #\\nYour job is to write like a human — clearly, simply, and professionally.Always write at a fifth-grade reading level.That means:Use short, direct sentencesUse simple words anyone can understandDon’t use corporate or tech-sounding languageDon’t use buzzwords, vague phrases, or marketing fluffNo emojisThis will be used to write professional emails, but the tone should feel casual and real, not formal or robotic. If it sounds like it came from a press release, a business meeting, or a PowerPoint slide — don’t write it. \\n\\n# STYLE #\\nNever use words or phrases like (this is a non-exhaustive list):\\ntarget industries, innovative, disruptive, scalable, solution, platform (unless clearly referring to software), synergy, leverage, ecosystem, holistic, agile, dynamic, end-to-end, seamless, best-in-class, mission-critical, bleeding edge, future-proof, strategic alignment, low-hanging fruit, optimize, empower, ideate, deep dive, robust, actionable insights, drive results, move the needle, streamline, scale up, pivot, thought leadership, utilize, enable, unlock value, deliver impact, unlock growth, customer-centric, business outcomes, value-add, cross-functional, verticals, stakeholders, circle back, touch base, bandwidth, core competency, KPIs, ROI \\n\\nInstead, speak like this:\\n“use” instead of “utilize”, “fix” instead of “resolve”, “team” instead of “cross-functional unit”, “help” instead of “empower”, “change” instead of “transform”, “tools” or “software” instead of “platform” \\n\\nThink like this: If a 10-year-old wouldn’t understand it, don’t say it.Your goal is to make things easy to read and easy to trust.\""
],
"Customer type (from LinkedIn and website)": [
"\"Determine the type of customer that this company usually sells to, using the two inputs as a guide for what they do. The 1st input is this: \\\"\\\"\" + {{LinkedIn Company Page}}?.description + \"\\\"\\\". The 2nd input is a website at \" + {{Company Domain}} + \". Who gets most value out of the product and what is their industry? Give me up to three types of customers. Do not include any numbers or extra information. Just a comma separated (i.e. X, Z, and Z) list of types of customers in the plural tense\\n\\nDo not get any data from a website that is not the input. Please think thoroughly about your process and take your time. It is very important that this is accurate and done well. My job is on the line that this done correctly. Your output should be a maximum of 10 words and not mention the company at all, but focus 100% on who their customer is\""
],
"Determine a company’s most frequent negative feedback": [
"\"Scrape the web to determine the most frequent negative comment this company, \" + {{input 1: Company name}} + \", receives. Output only the one negative feedback point. If you cannot find a frequent complaint, output Not found\""
],
"Determine a company’s target industries": [
"\"Scrape this site, \" + {{input 1: Domain}} + \" to determine what industries this company ideally would like to have as its customers. Output as many as its three top industries that it targets in a comma separated list. If you cannot determine which industries are its ideal targets, output Not found\""
],
"Determine a person's skill set": [
"\"Scrape this LinkedIn, , and output a comma separated list of their top quantifiable skills. You can list up to 5 of their top skills.\""
],
"Determine A Recent News Article is a Funding Announcement": [
"\"Assume the role of a financial news analyst to evaluate articles. \\n\\n1. Visit the URL of the article here: \" + {{input 1: link}} + \".\\n2. Determine if the article announces a new round of funding by a company.\\n3. Output 'True' if it does, and 'False' if it doesn't. \\n4. If True, extract and return:\\n a. Round of funding (e.g., seed, series A, series B).\\n b. Amount raised.\\n c. Company name.\\n d. Short description of the company's activities.\\n\\nIf the article is inaccessible, lacks relevant content, or talks about multiple companies, return 'False' and note \\\"Funding announcement not found\\\".\""
],
"Determine Company Revenue Models": [
"\"#CONTEXT#\\nYou are tasked with determining the revenue model(s) of a company using their company domain and specific steps to extract and analyze relevant information.\\n\\n#OBJECTIVE#\\nAnalyze the available resources to identify the revenue model(s) of the company associated with \" + {{input 1: Company Domain}} + \".\\n\\n#INSTRUCTIONS#\\n1. Analyze the company website \" + {{input 1: Company Domain}} + \":\\n - Check pricing, plans, or product pages for details on how they charge customers.\\n - Look for terms like subscription, per-seat pricing, commission, one-time payment, pay-as-you-go, freemium, revenue share, etc.\\n2. Search for relevant business model information on LinkedIn:\\n - Look for company posts, employee descriptions, or funding announcements mentioning their revenue structure.\\n3. Conduct a Google search for external sources:\\n - \\\"site:crunchbase.com \" + {{input 1: Company Domain}} + \" revenue model\\\"\\n - \\\"site:techcrunch.com \" + {{input 1: Company Domain}} + \" pricing model\\\"\\n - \\\"site:businessinsider.com \" + {{input 1: Company Domain}} + \" business model\\\"\\n - \\\"site:linkedin.com \" + {{input 1: Company Domain}} + \" pricing\\\"\\n - \\\"how does \" + {{input 1: Company Domain}} + \" make money\\\"\\n4. Extract the revenue model based on findings:\\n - If multiple models exist, list all relevant ones.\\n - If no model is explicitly stated, infer based on industry standards and similar companies.\\n\\n#EXAMPLES#\\nExample input: \\\"example.com\\\"\\nExpected output: \\\"Subscription, Transactional\\\"\\n\\n#OUTPUT RULES#\\n- Return only the revenue model(s) in a comma-separated list (e.g., \\\"Subscription, Transactional, Pay-per-use\\\"). Return the revenue model[s] and nothing else.\\n- If no revenue model can be determined, return \\\"Not found\\\" and nothing else.\""
],
"Determine if a company has ever experienced a data leak": [
"\"Scan the web to determine if this company, \" + {{input 1: Company Name}} + \", has ever had a data leak. If it has, output True, if it hasn’t output False. Do not output anything else\""
],
"Determine if a company is a retail company": [
"\"Scrape this company’s site to determine if the company with this domain, \" + {{input 1: Company Domain}} + \", is a retail company. If it is a retail company, return \\\"True\\\", otherwise return \\\"False\\\"\""
],
"Determine if a company is SaaS": [
"\"Based on the following company description, is this company a software as a service company? A software as a service company is a company that offers a software, usually for a monthly or annual subscription, to multiple users providing them with a service. This is the company description: \" + {{input 1}} + \"\\n Only return a result as True if it is a software as a service company or False if it is not a software as a service company.\""
],
"Determine if a company sells a normal or inferior good": [
"\"Determine if this company, \" + {{input 1: Company name}} + \", sells a normal or inferior good. Their good is normal if they sell more of it during an economic upturn, and their good is inferior if they sell more during an economic recession. Either output Normal good or Inferior good. If you cannot determine what type of good they sell, output Not found\""
],
"Determine if a person has ever worked for a certain company": [
"\"Check this LinkedIn \" + {{input 1: LinkedIn Profile}} + \" to see if this person has ever worked for \" + {{input 2: Company Name}} + \". If they have, output True, otherwise output False\""
],
"Determine if a person recently graduated": [
"\"Look at this LinkedIn, \" + {{input 1: Url}} + \", and determine if this person graduated from a university in the last two years. If they did, output “Graduated from (name of where they graduated from) (number of months since graduation) ago”. If they did not, output No.\""
],
"Determine industry of company": [
"\"Visit the website of this company at: \" + {{input 1: Company Domain}} + \", and additionally do general research on the web to determine its industry.\\n\\nDo not return anything else other than the industry.\""
],
"Determine industry trends": [
"\"Scrape the web to determine a trend in the following industry: \" + {{input 1: industry}} + \". Output just one sentence that says what the trend is.\""
],
"Determine number of employees according to LinkedIn": [
"\"Look at this company’s LinkedIn, \" + {{input 1: LinkedIn URL}} + \", and find the number of employees it says they have. Output only the number of employees listed. Do not preface the number with anything and do not include any other information. If you cannot find the number, output Not found\""
],
"Determine Someone's Likely Manager": [
"\"\\nIdentify the most likely direct manager of a given person based on their role (\" + {{input 1: Title}} + \") and company (\" + {{input 2: Company Domain}} + \").\\n\\nInstructions:\\nSearch for the individual’s role and company to determine their department and where they fit in the company’s hierarchy.\\nFind others in the same company with similar or related titles to establish the department’s reporting structure.\\nIdentify the most likely boss, prioritizing:\\nSomeone one level above in the same department (e.g., a \\\"Head of Growth\\\" for a \\\"Growth Manager\\\").\\nIf no direct manager is found, look for a department leader (e.g., \\\"VP of Growth\\\" or \\\"VP of Marketing\\\" for \\\"Growth Lead\\\").\\nIf neither is found, look for an executive overseeing the function (but avoid defaulting to the CEO unless they directly oversee the role).\\nVerify the person works at the company by checking their listed employer against /company_domain. Ignore results where the company does not match.\\nEnsure the LinkedIn profile URL is correctly formatted in the structure linkedin.com/in/username. Ignore results with different formats (e.g., company pages or incorrect URLs).\\nRetrieve their full name, job title, and LinkedIn profile URL.\\nOutput Format:\\nBoss’s Name: [Full Name]\\nInput_1: [Job Input_1]\\nLinkedIn URL: [linkedin.com/in/username]\\nExample Input:\\n/role: Growth Manager\\n/company name: Acme Inc.\\n/company_domain: acme.com\\nExample Output:\\nBoss’s Name: Jane Doe\\nInput_1: Head of Growth\\nLinkedIn URL: https://www.linkedin.com/in/janedoe\\n\""
],
"Determine trend of a company's YoY valuation change": [
"\"Scan the web to determine if this company, \" + {{input 1: name}} + \", is valued at a higher or lower amount than it was a year ago. Output either \\\"Higher\\\", \\\"Lower\\\", or \\\"Equal\\\" based on what you find, or output \\\"Not Found\\\" if you cannot find the information.\""
],
"Discover B2B Domain's Client Showcase Page": [
"\"Assume the role of a web domain expert tasked with identifying specific company webpage URLs. \\n- Begin by visiting the homepage of the website provided in {{Your Domain Column Here}} .\\n- Search for a section labeled \\\"Customers\\\", \\\"Customer Stories\\\", \\\"Clients\\\" or similar variations\\n- Click through navigational links or use a website's search function if available, to locate the full domain URL of this customer page.\\n- Ensure the URL directly leads to a dedicated customers or client list page, verifying it's under the same domain.\\n- Output the complete URL in JSON format.\\n\\nIf the page cannot be found, return \\\"Customer page not found\\\" as the output.\""
],
"Domain Registration Date": [
"\"You are a data investigator, best known for your ability to find out when company domains have been registered. \\nYou are going to go to \" + {{input 1: Domain Lookup URL}} + \" and find the Registered On date. This will be the date that \" + {{input 2: Domain [Cleaned]}} + \" was registered. Make sure you find this date as it is crucial information for my database.\\n\\nLook on the \" + {{input 1: Domain Lookup URL}} + \" page and make sure you return the date the domain was registered. This will be located under the \\\"Domain Registration\\\" section of the page, where it says \\\"Regsitered On\\\" - this is where you will find the date on the site.\\nExhaust all options to make sure you find this information. \\n\\nIf you still cannot find it, search google for the date that \" + {{input 2: Domain [Cleaned]}} + \" was registered. Look through the first 20 results on google.com.\\n\\nReturn only the date and nothing else.\\n\\nif after all of this you still cannot find the date, then return \\\"Not found\\\" and nothing else.\""
],
"Domain Validation": [
"\"Task: Verify the validity of a given domain and classify its status.\\n\\nHere's the domain to verify: \" + {{input 1: Domain}} + \"\\n\\nSummary of Actions:\\n\\n- Access the domain and check its response.\\n- Determine if the domain is active, returns a 404 error, is parked, or is a redirect.\\n- Ensure the domain is not an email address and is a properly formatted website domain.\\n- Ensure the domain is not a google.com/maps link. That does not represent a businesses website. \\n- Classify the domain based on its status.\\n\\nDetailed Step-by-Step:\\n1. Domain Access: Start by navigating to the domain URL provided.\\n2. Response Check:\\n- Observe the response when the domain is accessed.\\n- If the page loads normally, proceed to step 3.\\n- If the domain returns a \\\"404 Not Found\\\" error, classify it as a \\\"404 Error.\\\"\\n- If the domain redirects to a generic hosting provider page (e.g., GoDaddy, Bluehost), or shows content indicating that it is \\\"For Sale\\\" or \\\"Parked,\\\" classify it as a \\\"Parked Domain.\\\"\\n- If the domain redirects to a non-generic hosting provider and it is a valid business website, return the website.\\n\\nContent Verification:\\n- If the domain loads, verify that the content is genuine and not a placeholder or domain parking page.\\n- Check for indicators of an active, operational website, such as legitimate company information, products, services, or blog content.\\n- If the domain shows only a placeholder or minimal content (indicating it might be parked or inactive), classify it as \\\"Parked Domain.\\\"\\n\\nSecondary Checks:\\n- Use online tools or commands (e.g., ping, whois, or domain lookup services) to further verify the status of the domain if uncertain.\\n- If the domain is listed for sale or shown on a hosting page without any real content, it is not a valid, active domain.\\n\\nConstraints:\\n- Focus on determining whether the domain is actively being used.\\n- Ignore any domains that are clearly placeholders or listed for sale without real content.\\n- Consider a domain valid only if it displays a functioning website with meaningful content. Email addresses should return \\\"Invalid\\\". - Domains that contain google.com/maps should return \\\"Invalid\\\":\\n\\nOutput Format:\\n- Return \\\"Valid Domain\\\" if the domain is active with real content.\\n- Return \\\"404 Error\\\" if the domain returns a 404 error.\\n- Return \\\"Parked Domain\\\" if the domain is a placeholder, for sale, or redirects to a generic page.\\n- Return \\\"Invalid\\\" if the input is not a properly formatted domain (email addresses, google maps links, etc...)\\n- If the domain redirects to a non-generic hosting provider and it is a valid business website, return the website it redirects to.\\n\\nDo not return anything outside one of these 5 options.\\n\\n\""
],
"Email classification": [
"\"Objective:\\n\\nI received an email from someone with the email domain set to what I'll provide below. I want to determine whether its likely a real work email, or if they are just using a personal email or school email.\\n\\nFor the purposes of this research - Personal emails and school emails are typically on domains that are in one of these buckets:\\n- University or Colleges\\n- Domains with .edu in them\\n- ISP\\n- Search Engines\\n- Phone operators\\n- Telecommunications provider\\n- Email service provider\\n- Web Portal\\n- Email provider\\n- Email hosting service\\n- Spam Email provider \\n- web news site\\n- News portal\\n- Temporary Email provider\\n- Non existent\\n- Freemail or free email\\n\\nEven if the domain shows the company is a legitimate company - it still may fall into one of the buckets above and should be listed as a personal email. \\n\\nIf the domain is often used for email hosting and the general public can make an email on the domain, its a personal email. \\n\\nIf the domain is associated with a university or college it should also be listed as a personal email. These aren't technically work emails.\\n\\nBut that list is not exhaustive. I'm specifically trying to exclude companies that fall into one of those buckets (Or similar buckets) - as these often allow people to create public personal emails. Telecommunications providers often are used for personal emails which is why i'd like those listed as personal emails. Same with College and university emails - these typically aren't true work emails and should be listed as personal emails.\\n\\nIf the company is likely a Work Email, return Work Email. If an email on that domain is likely a Personal Email, return Personal Email.\\n\\nInstructions:\\n\\nGoogle \\\"What is + Domain\\\" for more information. If you see results that suggest it falls into the buckets I provided above, return Personal Email. If you see research that suggests the domain I provide you likely just provides Email services or could be used for a personal email, return Personal Email. You can also google \\\" Is + Domain+ a spam email provider\\\" to see if something could be used for personal email spam.\\n\\nOtherwise return Work Email.\\n\\nInput: \\n\\nDomain: \" + {{input 1: Work Email}}"
],
"Enrich to get Company Profile": [
"\"```\\n## Overall Goal (Firmographics Only; Domain Already Validated) ##\\nReturn verified firmographics for the target organization using the **validated domain** and **validated names** provided from the previous step. Do NOT re-validate or re-canonicalize domains. Use the provided values strictly to match the correct entity and extract:\\n- LinkedIn company URL\\n- HQ address (full + components)\\n- HQ phone and email\\n- Company description\\n- Industry\\n- NAICS code + NAICS industry name\\n- Founding year\\n- Company size (enum)\\n- Company status (Active / Inactive / Cannot Determine)\\n- Latest official press release URLs (array; newest first)\\n- Latest official financial statement/report URLs (array; newest first)\\n**No domain validation in this step.** If evidence conflicts with the provided validated inputs, stop and return \\\"Cannot Determine\\\" for `company_status` and leave unknown fields as `\\\"Not found\\\"` with sources explaining the mismatch.\\n---\\n## Execution Guarantees (MUST)\\n- You have full permission to run actions. Execute now; do not ask.\\n- Before returning any `\\\"Not found\\\"`, you MUST have executed either:\\n - ≥1 `visitWebPage` against an **official** page on the validated domain (e.g., /about, /contact, /privacy, /imprint), OR\\n - ≥3 distinct `searchGoogle` executions (each followed by ≥1 `visitWebPage`).\\n- Log each action you actually executed in `steps_taken_description` (one line per action, chronological). When multiple LinkedIn candidates appear for a domain query, log one visited candidate per step. If a candidate is rejected, include the concrete reason (Website mismatch / person page / location mismatch).\\n- Preserve LinkedIn URLs exactly as visited (including encoding, locale subdomains, trailing slash).\\n---\\n## Evidence & Priority Rules (No Domain Work Here)\\nAuthoritative evidence hierarchy:\\n1) **Pages on the validated domain** (About/Contact/Privacy/Imprint/Investor Relations/Newsroom).\\n2) **LinkedIn company org page** for this entity.\\n3) **Official filings or reputable registries** (SEC/EDGAR, Companies House, etc.) for founding year/status (when applicable).\\n4) **Well-known business directories** only for secondary confirmation (never override 1–2).\\nPress & financials:\\n- Prefer on-domain paths that clearly indicate **press, news, media, newsroom**.\\n- Prefer investor relations pages (**investors**, **ir**, **financials**, **reports**, **annual report**, **financial statements**, **10-K/20-F**).\\n- Return **arrays of URLs** (newest first). If dates are visible, choose the most recent based on the **current date** (determine via system time; if unavailable, run a quick date query and parse). For example, current year is 2025, so I would searchGoogle to query and get that latest value: what is today's date?\\n- If the company is private and no financials are published, return `\\\"Not found\\\"` for financials (do not use third-party PDFs unless clearly hosted by the company or an official regulator).\\nLinkedIn acceptance:\\n- Must be an **organization** page (`/company/...` or `/school/...`), not a person.\\n- Name/activity/location must cohere with the provided validated names and location hints.\\n- If Website is present on LinkedIn, you may use it to cross-check the entity, **but do not change the validated domain**.\\n- Prefer LinkedIn org pages whose “Website” field eTLD+1 == {validated_domain}; treat “inactive” pages as acceptable if they satisfy that rule. Do NOT change {validated_domain} based on LinkedIn.\\nFormatting/standardization:\\n- Phone numbers: include country code where possible (e.g., +1-###-###-####).\\n- State/country written out in full (e.g., \\\"California\\\", \\\"United States\\\").\\n- Company size must match the provided enum exactly.\\n---\\n## Action Model (Single-Action Sequencing)\\nProceed one action at a time:\\n`searchGoogle` → `visitWebPage` → decide next → repeat.\\n**Suggested flow**\\n1) **Recency anchor**: Determine today’s date (system clock; else quick query). Use it to judge “latest”. For example, current year is 2025, so I would searchGoogle to query and get that latest value: what is today's date?\\n2) **On-site extraction** (validated domain):\\n - Visit `https://{validated_domain}/` then try `/about`, `/contact`, `/privacy`, `/imprint`, `/investors`, `/investor-relations`, `/news`, `/press`, `/media`, `/financials`, `/reports`.\\n - Extract HQ address (full + components), phone, email, company description.\\n3) **LinkedIn (Domain-first resolution)\\nRun ONE query at a time (no quotes/operators to start), then visit exactly ONE best result after each query:\\n Q1: {validated_domain} LinkedIn\\n Q2: {normalized_company_name} LinkedIn\\n Q3: {legal_company_name} LinkedIn\\n (Fallback, only if needed and allowed) Q4: site:linkedin.com/company {validated_domain}\\n (Fallback, only if needed and allowed) Q5: site:linkedin.com {validated_domain}\\nCandidate acceptance (must satisfy ALL):\\n [L1] Page is an ORGANIZATION page (/company/... or /school/...), not a person.\\n [L2] If the LinkedIn “Website” field is visible, its eTLD+1 == {validated_domain} (ignore scheme, www).\\n [L3] Name/activity/location cohere with provided validated names and any location hints.\\nTie-breakers (when ≥2 candidates satisfy L1–L3):\\n T1. Prefer the candidate whose Website field EXACTLY matches {validated_domain}.\\n T2. If both match, prefer the one with richer org metadata visible (about text, size, HQ).\\n T3. If still tied, prefer the slug that most clearly reflects the brand (e.g., /company/tadawimedicalcenter over /company/tadawi-medical-center).\\n T4. If still tied, prefer the page with visible HQ city matching on-site address.\\nRejections (log each in sources.linkedin_url):\\n - Reject if Website domain ≠ {validated_domain} → “Rejected: LinkedIn Website mismatch (shows X, expected {validated_domain}).”\\n - Reject person profiles or unrelated entities → “Rejected: person page / different entity.”\\nStop rule:\\n - As soon as a candidate passes L1–L3, keep the EXACT visited URL (preserve locale/encoding/trailing slash) and STOP LinkedIn searching.\\n4) **Industry & NAICS**:\\n - From site + LinkedIn description, derive primary activity.\\n - Map to NAICS using **industry terms only** (not company name).\\n5) **Founding year & status**:\\n - Prefer official site “About/History”.\\n - For public cos, use regulator filings (e.g., SEC/EDGAR).\\n - Otherwise, LinkedIn/About if consistent.\\n6) **Press & Financials**:\\n - Prioritize official on-domain newsroom/press pages and investor sections.\\n - Return arrays of the latest URLs (newest first). If no items, `\\\"Not found\\\"`.\\nStop when fields are filled with sufficient evidence; otherwise continue until the minimum work floor is met.\\n---\\n## Acceptance Checklist (Before Return)\\n- No domain canonicalization performed; the provided validated inputs were used only for matching.\\n- LinkedIn URL preserved exactly as visited.\\n- Address components populated consistently; `full_address` matches components.\\n- Phone/email belong to the company (not directory support numbers).\\n- Industry and NAICS pair are coherent.\\n- Press and financial URLs are official and ordered newest-first.\\n- Every non-default field has ≥1 supporting source line.\\n---\\n## Return Format (Single JSON Object; no prose)\\n- Use **exact keys** and types below.\\n- Defaults: Strings → `\\\"Not found\\\"`, Arrays → `[]`, Objects → `{}`.\\n- `additionalProperties: false`.\\n```\\n```json\\n{\\n \\\"type\\\": \\\"object\\\",\\n \\\"additionalProperties\\\": false,\\n \\\"required\\\": [\\n \\\"full_address\\\",\\n \\\"address_components\\\",\\n \\\"hq_phone\\\",\\n \\\"hq_email\\\",\\n \\\"linkedin_url\\\",\\n \\\"company_description\\\",\\n \\\"industry\\\",\\n \\\"naics_code\\\",\\n \\\"naics_industry\\\",\\n \\\"founding_year\\\",\\n \\\"company_size\\\",\\n \\\"company_status\\\",\\n \\\"press_release_urls\\\",\\n \\\"financial_statement_urls\\\",\\n \\\"steps_taken_description\\\",\\n \\\"sources\\\"\\n ],\\n \\\"properties\\\": {\\n \\\"full_address\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"address_components\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"additionalProperties\\\": false,\\n \\\"required\\\": [\\\"street\\\", \\\"city\\\", \\\"state\\\", \\\"postal_code\\\", \\\"country\\\"],\\n \\\"properties\\\": {\\n \\\"street\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"city\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"state\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"postal_code\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"country\\\": { \\\"type\\\": \\\"string\\\" }\\n }\\n },\\n \\\"hq_phone\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"hq_email\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"linkedin_url\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"company_description\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"industry\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"naics_code\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"naics_industry\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"founding_year\\\": { \\\"type\\\": \\\"string\\\" },\\n \\\"company_size\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"enum\\\": [\\n \\\"Self-employed\\\",\\n \\\"2-10 employees\\\",\\n \\\"11-50 employees\\\",\\n \\\"51-200 employees\\\",\\n \\\"201-500 employees\\\",\\n \\\"501-1,000 employees\\\",\\n \\\"1,001-5,000 employees\\\",\\n \\\"5,001-10,000 employees\\\",\\n \\\"10,001+ employees\\\",\\n \\\"Not found\\\"\\n ]\\n },\\n \\\"company_status\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"enum\\\": [\\\"Active\\\", \\\"Inactive\\\", \\\"Cannot Determine\\\"]\\n },\\n \\\"press_release_urls\\\": {\\n \\\"type\\\": \\\"array\\\",\\n \\\"items\\\": { \\\"type\\\": \\\"string\\\" }\\n },\\n \\\"financial_statement_urls\\\": {\\n \\\"type\\\": \\\"array\\\",\\n \\\"items\\\": { \\\"type\\\": \\\"string\\\" }\\n },\\n \\\"steps_taken_description\\\": {\\n \\\"type\\\": \\\"array\\\",\\n \\\"items\\\": { \\\"type\\\": \\\"string\\\" }\\n },\\n \\\"sources\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"additionalProperties\\\": false,\\n \\\"required\\\": [\\n \\\"full_address\\\",\\n \\\"address_components\\\",\\n \\\"hq_phone\\\",\\n \\\"hq_email\\\",\\n \\\"linkedin_url\\\",\\n \\\"company_description\\\",\\n \\\"industry\\\",\\n \\\"naics_code\\\",\\n \\\"naics_industry\\\",\\n \\\"founding_year\\\",\\n \\\"company_size\\\",\\n \\\"company_status\\\",\\n \\\"press_release_urls\\\",\\n \\\"financial_statement_urls\\\"\\n ],\\n \\\"properties\\\": {\\n \\\"full_address\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"address_components\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"additionalProperties\\\": false,\\n \\\"required\\\": [\\\"street\\\", \\\"city\\\", \\\"state\\\", \\\"postal_code\\\", \\\"country\\\"],\\n \\\"properties\\\": {\\n \\\"street\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"city\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"state\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"postal_code\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"country\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } }\\n }\\n },\\n \\\"hq_phone\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"hq_email\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"linkedin_url\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"company_description\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"industry\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"naics_code\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"naics_industry\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"founding_year\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"company_size\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"company_status\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"press_release_urls\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } },\\n \\\"financial_statement_urls\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"string\\\" } }\\n }\\n }\\n }\\n}\\n```\\nExample — LinkedIn via domain query (tadawi.me)\\nInputs: validated_domain = tadawi.me; normalized_company_name = \\\"Tadawi Medical Center\\\"\\nFlow:\\n - searchGoogle: \\\"tadawi.me LinkedIn\\\" → two org candidates appear\\n - visitWebPage: https://www.linkedin.com/company/tadawimedicalcenter/ (Website: https://tadawi.me/)\\n ✓ L1..L3 pass → keep EXACT URL; stop searching\\nOutput snippet:\\n \\\"linkedin_url\\\": \\\"https://www.linkedin.com/company/tadawimedicalcenter/\\\"\\nSources:\\n linkedin_url → [\\\"https://www.linkedin.com/company/tadawimedicalcenter/ - Snippet: Website field shows https://tadawi.me/\\\"]\\n\\n```\\n## Available Inputs (use what you have from this section only)\\n- **Validated Company Domain** → [\"+{{input 1: Validated Domain}}+\"] → `{validated_domain}` // e.g., \\\"example.com\\\"; anchor for on-site extraction\\n- **Legal Company Name** → [\"+{{input 2: Legal Company Name}}+\"] → `{legal_company_name}` // validated upstream; disambiguation only\\n- **Normalized Company Name** → [\"+{{input 3: Normalized Company Name}}+\"] → `{normalized_company_name}` // validated upstream; disambiguation only\\n- **Country** → [\"+{{input 4: In- Country}}+\"] → `{country}`\\n- **City/Region** → [\"+{{input 5: In- City}}+\"] → `{city}`\\n- **Industry** → [\"+{{input 6: In- Industry}}+\"] → `{input_industry}`\\n- **Current date** → [\"+{{input 7: Created At}}+\"] → `{current_date}` use to determine file recency if requested, or to run a date confirmation query as indicated for this purpose.\\n**Rules:**\\n- Do **not** alter or re-validate `{validated_domain}`.\\n- Use names + location only to confirm you’re on the right org.\\n- If strong conflict with validated inputs, set `company_status = \\\"Cannot Determine\\\"`, keep fields `\\\"Not found\\\"`, and record the mismatch in `sources`.\\n---\\n```\""
],
"Entity Normalization": [
"\"# Company Entity Normalization AI Prompt\\n\\n## System Prompt\\nYou are an expert company entity normalization specialist. Your task is to process corporate hierarchy JSON data and normalize company entities while extracting standardized location components. You will output three separate arrays: one for the input company, one for parent companies, and one for subsidiary companies.\\n\\n## Input Definition\\nYou will receive a JSON object containing corporate hierarchy information with the following structure:\\n```json\\n{\\n \\\"reasoning\\\": \\\"...\\\",\\n \\\"confidence_level\\\": \\\"...\\\",\\n \\\"input_company\\\": [\\n {\\n \\\"level\\\": \\\"L3\\\",\\n \\\"legal_name\\\": \\\"LinkedIn Ireland Unlimited Company\\\",\\n \\\"hq_territory\\\": \\\"Dublin, Ireland\\\",\\n \\\"company_domain\\\": \\\"linkedin.com\\\"\\n }\\n ],\\n \\\"parent_hierarchy\\\": [\\n {\\n \\\"level\\\": \\\"L1\\\",\\n \\\"legal_name\\\": \\\"Microsoft Corporation\\\",\\n \\\"hq_territory\\\": \\\"Redmond, Washington, United States\\\",\\n \\\"company_domain\\\": \\\"microsoft.com\\\"\\n },\\n {\\n \\\"level\\\": \\\"L2\\\",\\n \\\"legal_name\\\": \\\"Microsoft Ireland Research\\\",\\n \\\"hq_territory\\\": \\\"Dublin, Ireland\\\",\\n \\\"company_domain\\\": \\\"microsoft.com\\\"\\n }\\n ],\\n \\\"subsidiary_hierarchy\\\": [\\n {\\n \\\"legal_name\\\": \\\"LinkedIn Singapore Pte Ltd\\\",\\n \\\"hq_territory\\\": \\\"Singapore\\\",\\n \\\"company_domain\\\": \\\"linkedin.com\\\"\\n }\\n ]\\n}\\n```\\n\\n## Input Data\\n**Company Hierarchy Data for Processing**: \"+{{input 1: Corporate Hierarchy JSON}}+\"\\n\\n## Output Format\\nProcess entities and provide normalized data in three separate arrays:\\n```json\\n{\\n \\\"input_company\\\": [\\n {\\n \\\"original_level\\\": \\\"L3\\\",\\n \\\"legal_name_original\\\": \\\"LinkedIn Ireland Unlimited Company\\\",\\n \\\"company_name\\\": \\\"LinkedIn Ireland Unlimited\\\", \\n \\\"tax_structure\\\": \\\"Co\\\",\\n \\\"city\\\": \\\"Dublin\\\",\\n \\\"state_province\\\": \\\"\\\",\\n \\\"country\\\": \\\"Ireland\\\",\\n \\\"country_code\\\": \\\"IE\\\", \\n \\\"normalized_entity_name\\\": \\\"LinkedIn Ireland Unlimited Co\\\",\\n \\\"hq_territory_original\\\": \\\"Dublin, Ireland\\\",\\n \\\"company_domain\\\": \\\"linkedin.com\\\"\\n }\\n ],\\n \\\"parent_companies\\\": [\\n {\\n \\\"original_level\\\": \\\"L1\\\",\\n \\\"legal_name_original\\\": \\\"Microsoft Corporation\\\",\\n \\\"company_name\\\": \\\"Microsoft\\\",\\n \\\"tax_structure\\\": \\\"Corp\\\",\\n \\\"city\\\": \\\"Redmond\\\",\\n \\\"state_province\\\": \\\"Washington\\\", \\n \\\"country\\\": \\\"United States\\\",\\n \\\"country_code\\\": \\\"US\\\",\\n \\\"normalized_entity_name\\\": \\\"Microsoft Corp\\\",\\n \\\"hq_territory_original\\\": \\\"Redmond, Washington, United States\\\",\\n \\\"company_domain\\\": \\\"microsoft.com\\\"\\n },\\n {\\n \\\"original_level\\\": \\\"L2\\\",\\n \\\"legal_name_original\\\": \\\"Microsoft Ireland Research\\\", \\n \\\"company_name\\\": \\\"Microsoft Ireland Research\\\",\\n \\\"tax_structure\\\": \\\"\\\",\\n \\\"city\\\": \\\"Dublin\\\",\\n \\\"state_province\\\": \\\"\\\",\\n \\\"country\\\": \\\"Ireland\\\", \\n \\\"country_code\\\": \\\"IE\\\",\\n \\\"normalized_entity_name\\\": \\\"Microsoft Ireland Research\\\",\\n \\\"hq_territory_original\\\": \\\"Dublin, Ireland\\\",\\n \\\"company_domain\\\": \\\"microsoft.com\\\"\\n }\\n ],\\n \\\"subsidiary_companies\\\": [\\n {\\n \\\"legal_name_original\\\": \\\"LinkedIn Singapore Pte Ltd\\\",\\n \\\"company_name\\\": \\\"LinkedIn Singapore\\\",\\n \\\"tax_structure\\\": \\\"Pte Ltd\\\",\\n \\\"city\\\": \\\"Singapore\\\",\\n \\\"state_province\\\": \\\"\\\",\\n \\\"country\\\": \\\"Singapore\\\",\\n \\\"country_code\\\": \\\"SG\\\",\\n \\\"normalized_entity_name\\\": \\\"LinkedIn Singapore Pte Ltd\\\",\\n \\\"hq_territory_original\\\": \\\"Singapore\\\",\\n \\\"company_domain\\\": \\\"linkedin.com\\\"\\n }\\n ]\\n}\\n```\\n\\n## Important Processing Logic\\n\\n### Entity Classification\\n- **Input Company**: Process the entity from the `input_company` array (includes level field)\\n- **Parent Companies**: Process all entities from the `parent_hierarchy` array (these are companies above the input company)\\n- **Subsidiary Companies**: Process all entities from the `subsidiary_hierarchy` array (these are immediate subsidiaries of the input company)\\n\\n### Level Information\\n- Parent companies retain their `original_level` field from the parent_hierarchy\\n- **Input company retains its `original_level` field from the input_company array**\\n- Subsidiaries do not have level fields as they are all one level below the input company\\n## Processing Rules\\n### Legal Name Processing\\n1. **Parse the complete legal_name** to extract:\\n - Company name (core business identity)\\n - Tax structure (legal entity suffix)\\n2. **Apply company name standardization rules**\\n3. **Apply tax structure standardization from the lookup table**\\n\\n### Domain Processing\\n- Include the `company_domain` field from the input data for each entity\\n- Pass through domain values as provided without modification\\n\\n### Location Processing\\nParse `hq_territory` to extract location components using these patterns:\\n**Three-part format**: `\\\"City, State/Province, Country\\\"`\\n- Example: `\\\"Burbank, California, United States\\\"` \\n- Extract: city=\\\"Burbank\\\", state_province=\\\"California\\\", country=\\\"United States\\\"\\n**Two-part format**: `\\\"City, Country\\\"` \\n- Example: `\\\"Sao Paulo, Brazil\\\"`\\n- Extract: city=\\\"Sao Paulo\\\", state_province=\\\"\\\", country=\\\"Brazil\\\"\\n**Single location**: `\\\"Country\\\"` or `\\\"City\\\"`\\n- Example: `\\\"Singapore\\\"`\\n- Extract based on context (if it's a known country, assign to country field)\\n\\n### Location Standardization Rules\\n**Country Name Standardization:**\\n- `\\\"United States\\\"`, `\\\"USA\\\"`, `\\\"America\\\"` → `\\\"United States\\\"`\\n- `\\\"United Kingdom\\\"`, `\\\"UK\\\"`, `\\\"Britain\\\"`, `\\\"England\\\"` → `\\\"United Kingdom\\\"` \\n- Keep other countries as full names: `\\\"Brazil\\\"`, `\\\"Argentina\\\"`, `\\\"India\\\"`, etc.\\n**State/Province Standardization:**\\n- Use full names: `\\\"California\\\"`, `\\\"New York\\\"`, `\\\"Ontario\\\"`, etc.\\n- Do not abbreviate unless that's the standard form\\n**City Standardization:**\\n- Use proper title case: `\\\"New York\\\"`, `\\\"Los Angeles\\\"`, `\\\"São Paulo\\\"`\\n- Preserve international characters and diacritics\\n\\n## Core Normalization Rules\\n\\n### Company Name Standardization\\n**ALWAYS Apply These Standardizations:**\\n1. **Conjunction Standardization**: Convert \\\"and\\\" to \\\"&\\\" in company names\\n - `\\\"Marcus and Millichap\\\"` → `\\\"Marcus & Millichap\\\"`\\n - `\\\"Smith and Associates\\\"` → `\\\"Smith & Associates\\\"`\\n2. **Spacing Normalization**: Remove excessive whitespace, standardize spacing around symbols\\n - `\\\"Company Inc\\\"` → `\\\"Company Inc\\\"`\\n - `\\\"Company&Associates\\\"` → `\\\"Company & Associates\\\"`\\n - `\\\"Company & Associates\\\"` → `\\\"Company & Associates\\\"`\\n3. **Capitalization Standardization**: Convert to proper title case with exceptions\\n - `\\\"MARCUS & MILLICHAP\\\"` → `\\\"Marcus & Millichap\\\"`\\n - **Preserve known acronyms**: `\\\"IBM\\\"`, `\\\"AT&T\\\"`, `\\\"HP\\\"`, `\\\"IT\\\"`, `\\\"AI\\\"`, `\\\"API\\\"`\\n - **Preserve brand formatting**: `\\\"iPhone\\\"`, `\\\"eBay\\\"`, `\\\"PayPal\\\"`, `\\\"Neo@Ogilvy\\\"`\\n4. **Article Removal**: Remove leading articles only if not part of brand identity\\n - `\\\"The Home Depot\\\"` → `\\\"Home Depot\\\"` (article not essential)\\n5. **Data Quality Marker Removal**: Remove obvious data quality flags\\n - `\\\"Company - (DUPE)\\\"` → `\\\"Company\\\"`\\n - `\\\"Firm (TEST)\\\"` → `\\\"Firm\\\"`\\n - `\\\"Company (INACTIVE)\\\"` → `\\\"Company\\\"`\\n\\n### Tax Structure Standardization\\n**Use EXACT standardized forms from this 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\\\"`,`\\\"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- `\\\"Eoo\\\"`, `\\\"EAD\\\"` → Keep as provided for Bulgarian entities\\n- `\\\"Unlimited Company\\\"`, `\\\"Unlimited\\\"` → `\\\"Co\\\"` (Irish unlimited companies standardize to Co)\\n\\n### Country Code Standardization\\n**Convert to 2-letter ISO codes:**\\n- `\\\"United States\\\"`, `\\\"USA\\\"`, `\\\"America\\\"` → `\\\"US\\\"`\\n- `\\\"United Kingdom\\\"`, `\\\"UK\\\"`, `\\\"Britain\\\"`, `\\\"England\\\"` → `\\\"GB\\\"`\\n- `\\\"India\\\"`, `\\\"Bharat\\\"` → `\\\"IN\\\"`\\n- `\\\"Germany\\\"`, `\\\"Deutschland\\\"` → `\\\"DE\\\"`\\n- `\\\"France\\\"`, `\\\"Francia\\\"` → `\\\"FR\\\"`\\n- `\\\"Australia\\\"`, `\\\"Oz\\\"` → `\\\"AU\\\"`\\n- `\\\"Canada\\\"` → `\\\"CA\\\"`\\n- `\\\"Brazil\\\"`, `\\\"Brasil\\\"` → `\\\"BR\\\"`\\n- `\\\"Mexico\\\"`, `\\\"México\\\"` → `\\\"MX\\\"`\\n- `\\\"China\\\"`, `\\\"PRC\\\"` → `\\\"CN\\\"`\\n- `\\\"Japan\\\"`, `\\\"Nippon\\\"` → `\\\"JP\\\"`\\n- `\\\"South Africa\\\"` → `\\\"ZA\\\"`\\n- `\\\"Singapore\\\"` → `\\\"SG\\\"`\\n- `\\\"Indonesia\\\"` → `\\\"ID\\\"`\\n- `\\\"Netherlands\\\"`, `\\\"Holland\\\"` → `\\\"NL\\\"`\\n- `\\\"Switzerland\\\"`, `\\\"Schweiz\\\"` → `\\\"CH\\\"`\\n- `\\\"Sweden\\\"`, `\\\"Sverige\\\"` → `\\\"SE\\\"`\\n- `\\\"Ireland\\\"`, `\\\"Éire\\\"` → `\\\"IE\\\"`\\n- `\\\"Bulgaria\\\"` → `\\\"BG\\\"`\\n- `\\\"Austria\\\"`, `\\\"Österreich\\\"` → `\\\"AT\\\"`\\n- `\\\"Argentina\\\"` → `\\\"AR\\\"`\\n\\n### Normalized Entity Name Rules\\n**CRITICAL**: The normalized entity name should:\\n1. **Apply company name standardization** (& for and, title case, spacing)\\n2. **Apply tax structure standardization** \\n3. **Maintain geographic context when part of company identity**\\n4. **Remove data quality markers**\\n5. **Follow format: [Company Name] [Geographic Identifier (if part of identity)] [Tax Structure]**\\n**Example Transformations:**\\n- `\\\"The Walt Disney Company\\\"` → `\\\"Walt Disney Co\\\"`\\n- `\\\"FOX International Channels Argentina S.A.\\\"` → `\\\"FOX International Channels Argentina S.A.\\\"`\\n- `\\\"Disney Media Networks Latin America\\\"` → `\\\"Disney Media Networks Latin America\\\"`\\n- `\\\"LinkedIn Ireland Unlimited Company\\\"` → `\\\"LinkedIn Ireland Unlimited Co\\\"`\\n- `\\\"LinkedIn Singapore Pte Ltd\\\"` → `\\\"LinkedIn Singapore Pte Ltd\\\"`\\n\\n## Processing Guidelines\\n\\n### Entity Processing Workflow\\n1. **Process input company**: Extract and normalize the single entity from input_company array, preserving its level\\n2. **Process parent companies**: Iterate through parent_hierarchy array and normalize each\\n3. **Process subsidiary companies**: Iterate through subsidiary_hierarchy array and normalize each\\n4. **Parse legal names**: Split into company name and tax structure components \\n5. **Parse locations**: Extract city, state/province, and country from hq_territory\\n6. **Apply all standardization rules**: Company name, tax structure, country codes\\n7. **Generate normalized entity name**: Combine standardized components\\n8. **Preserve original values**: Keep original legal_name and hq_territory for reference\\n9. **Include domains**: Pass through company_domain field for each entity\\n10. **Maintain array structure**: Keep entities in their respective arrays\\n\\n### Quality Assurance Checklist\\nBefore finalizing each entity:\\n1. ✅ Is original_level correctly preserved from input data?\\n2. ✅ Is \\\"and\\\" → \\\"&\\\" conversion applied in company name and normalized entity name?\\n3. ✅ Is spacing normalized and title case applied appropriately?\\n4. ✅ Is tax structure using EXACT standardized form from the table?\\n5. ✅ Is country code the correct 2-letter ISO code?\\n6. ✅ Are city, state/province, and country properly extracted and formatted?\\n7. ✅ Are data quality markers removed from all components?\\n8. ✅ Is brand-specific formatting preserved where appropriate?\\n9. ✅ Are original values preserved for audit trail?\\n10. ✅ Is company_domain included for each entity?\\n11. ✅ Is the entity placed in the correct output array?\\n\\n## Error Prevention\\n\\n### DO NOT:\\n- Use tax structure variations not in the standardization table\\n- Lose geographic information when parsing locations\\n- Over-standardize unique brand elements (@, camelCase where part of brand)\\n- Include data quality markers (DUPE, TEST, INACTIVE) in any output\\n- Translate foreign language business names to English\\n- Mix entities between the three arrays\\n- Modify or standardize domain values\\n- Calculate or modify the level field - preserve it exactly as provided\\n\\n### ALWAYS: \\n- Use EXACT standardized forms from the tax structure table\\n- Parse all location components accurately from hq_territory\\n- Apply consistent company name standardization across all entities\\n- Maintain recognizable brand formatting in company name\\n- Use correct 2-letter ISO country codes\\n- Keep input_company, parent_companies, and subsidiary_companies in separate arrays\\n- Include company_domain field for all entities\\n- Process empty arrays (e.g., subsidiary_hierarchy: []) as empty output arrays\\n- Preserve the original_level field from input data (do not calculate or modify)\""
],
"Estimated Age": [
"\"#CONTEXT#\nYou are tasked with estimating a person's age using publicly available information, primarily from their professional or educational profiles. #OBJECTIVE#\nDetermine the person's age by analyzing their college graduation date or the start date of their first non-internship experience, using data from \" + {{input 1: LinkedIn}} + \". If this information is not available or clear, expand your search to other public sources using \" + {{input 2: name}} + \" or any other relevant information found on \" + {{input 1: LinkedIn}} + \". #INSTRUCTIONS#\n1. Visit the profile or page specified by \" + {{input 1: LinkedIn}} + \".\n2. Look for the college graduation date. If found, estimate the person's age by assuming a typical graduation age (e.g., 22 years old for undergraduate degrees) and adding the number of years since graduation.\n3. If the graduation date is not available, look for the start month and year or year of the first professional experience that is not an internship. Make the assumption they are starting this job after college at 22.\n4. If neither is available or clear, use \" + {{input 2: name}} + \" or any other information from \" + {{input 1: LinkedIn}} + \" to search other public sources (e.g., news articles, company bios, public records) for clues about the person's age.\n5. If you cannot find sufficient information to estimate the age, return \\\"Age not found\\\". #EXAMPLES#\nExample input:\n- \" + {{input 1: LinkedIn}} + \": https://www.linkedin.com/in/johndoe\n- \" + {{input 2: name}} + \": John Doe Expected output:\n- Estimated age: 34 If no data is found:\n- Age not found Example: Donna's first year of work is 2001 found on her \" + {{input 1: LinkedIn}} + \". If we assume she was 22 when she started in 2001, then she would be 46 this year.\""
],
"Extract city from address": [
"\"Given this address: \" + {{Address}} + \", extract and output the city it is in. Do not output zip codes, state, country or street address, just the city name\""
],
"Extract use case and objections from transcript": [
"\"#CONTEXT#\\nAnalyze the provided transcript to determine the prospect's intended use case with , excluding any input from Daniel. Also, include any objection reasons that wouldn't be able to solve this use case and include that in a separate output.\\n\\n#OBJECTIVE#\\nSummarize the prospect's core use case with in less than 15 words. \\n\\n#INSTRUCTIONS#\\n1. Carefully read through the transcript given below.\\n2. Identify statements made by the prospect that indicate their intended use or goals with .\\n3. Exclude any statements or input from Daniel in your analysis.\\n4. Formulate a concise summary of the prospect's core use case in a single sentence, ensuring it is under 15 words.\\n5. Identify any objections or reasons why might not be able to solve the use case and include them in a separate output. \\n\\n#EXAMPLES#\\nExample input: Transcript with multiple speakers including the prospect and Daniel.\\nExample output: \\\"Prospect aims to use for efficient data visualization and reporting.\\\"\\n\\n# RULES # \\nFor objection formatting - summarize their objection in <20 words without including any words like \\\"Potential objections\\\" or Potential limitations\\\" get right to the point.\\n\\nTranscript: \" + JSON.stringify({{input 1}})"
],
"Find & Normalize First Name from Instagram": [
"\"#CONTEXT#\\nYou are an AI specialized in text parsing and normalization. You will extract a first name from noisy input strings and normalize it to have only the first letter capitalized. If you determine there is no valid human first name present (e.g., the text represents a company or brand), return nothing.\\n\\n#OBJECTIVE#\\nExtract and return only the normalized first name from the provided inputs, or return nothing if no valid first name exists.\\n\\n#INSTRUCTIONS#\\n1. Primary input: use the content from \" + {{input 1: Full Name}} + \" as the main source to determine the first name.\\n2. Secondary inputs: if the primary input is missing, ambiguous, or insufficient, you may also reference \" + {{input 2: Username}} + \" and \" + {{input 3: Email}} + \" to help infer the first name. Do not introduce any other columns.\\n3. Cleaning steps for parsing the first token:\\n - Remove leading/trailing whitespace.\\n - Replace separators like |, /, \\\\, -, _, ·, •, commas, and multiple spaces with a single space.\\n - Remove emojis and most symbols, keeping only letters, apostrophes, and hyphens within words.\\n - Split the cleaned string into tokens by spaces.\\n4. Candidate selection rules:\\n - Consider the first token that starts with a letter (A–Z or a–z) as the candidate first name.\\n - Exclude tokens that are clearly non-names such as all-caps acronyms (length ≥2), URLs/handles (contain @ or .com or starts with http), numeric-leading tokens, or common brand/company indicators (e.g., LLC, Inc, Ltd, Shop, Store, Media, Agency, Studio, Labs, Official, Team).\\n - If the token looks like a role/descriptor (e.g., dev, founder, mom, dad, blogger) and not a given name, do not return it; instead, continue scanning subsequent tokens for a plausible given name.\\n5. Validation heuristics for a human first name:\\n - Length between 2 and 20 characters (after stripping non-letter characters except apostrophes/hyphens).\\n - Contains at least one vowel (a, e, i, o, u, y) unless it is a well-formed initial like a single letter followed by a period (which should be rejected).\\n - Not purely uppercase unless it is an initial (reject initials).\\n6. Normalization:\\n - Return the candidate in \\\"Firstletterlowerrestlower\\\" form (e.g., \\\"kRiStInA\\\" -> \\\"Kristina\\\").\\n - Preserve internal apostrophes and hyphens while applying capitalization to each sub-part (e.g., \\\"o'neill\\\" -> \\\"O'Neill\\\", \\\"jean-luc\\\" -> \\\"Jean-Luc\\\").\\n7. If no valid first name is found after applying the above rules, return nothing (empty output).\\n8. Output strictly the resulting first name string with correct capitalization, or nothing at all. Do not include any other text.\\n\\n#EXAMPLES#\\nInput: \\\"Gina | Mom life & products\\\" -> Output: \\\"Gina\\\"\\nInput: \\\"dev\\\" -> Output: \\\"\\\" \\nInput: \\\"Jenny//Handmade Style\\\" -> Output: \\\"Jenny\\\"\\nInput: \\\"ACME Inc | Official\\\" -> Output: \\\"\\\"\\nInput: \\\"o'neill crafts\\\" -> Output: \\\"O'Neill\\\"\\nInput: \\\"JEAN-LUC // creator\\\" -> Output: \\\"Jean-Luc\\\"\""
],
"Find # locations for company": [
"\"\\r\\nTask: Retrieve the total number of locations this company \" + {{input 1: Account Name}} + \" operates.\\n\\nSteps:\\nSearch for the company's official website \" + {{input 2: Consolidated Domain}} + \" using its full name (\" + {{input 1: Account Name}} + \").\\nNavigate to sections like \\\"About Us,\\\" \\\"Our Locations,\\\" or similar pages where location information might be listed. Also do a google search if you unable to find anything on the official website.\\nExtract the number of locations mentioned, and cross-check this information with other credible sources like business profiles or news articles.\\nSummarize the result in the following format: \\\"20\\\"\\nConstraints and Considerations:\\n\\nEnsure the information is up-to-date and from a reliable source.\\nDesired Output: Return only the total number of locations the company has\""
],
"Find a company's 10-K": [
"\"Your task is to finding the most recent 10-K PDF URL for \" + {{input 1: Company Name}} + \". Your goal is to return only the URL of the most recent 10-K PDF filing.\\n\\nHere is the company name: \" + {{input 1: Company Name}} + \"\\n\\nFollow these steps to find the 10-K PDF URL:\\n1. Google Search:\\n a. Perform a Google search using the following query: \\\"\" + {{input 1: Company Name}} + \" 10-K filetype:pdf\\\"\\n b. Look for results from the company's official investor relations website or the SEC's website (sec.gov)\\n c. If you find a direct link to the most recent 10-K PDF, proceed to step 4\\n2. EDGAR Search:\\n a. If the Google search doesn't yield results, go to the SEC's EDGAR database: https://www.sec.gov/edgar/searchedgar/companysearch\\n b. Enter the company name in the search box\\n c. Look for the most recent 10-K filing\\n d. Click on the \\\"Documents\\\" button for the most recent 10-K\\n e. Find the link to the complete submission text file (usually ends with \\\".htm\\\")\\n f. Open this file and search for a link ending with \\\".pdf\\\" that contains \\\"10-K\\\" in its name\\n g. If you find the PDF link, proceed to step 4\\n3. Broad Search:\\n a. If both previous methods fail, perform a broader internet search\\n b. Look for the company's investor relations website\\n c. Navigate to their SEC filings or financial reports section\\n d. Locate the most recent 10-K report and find a link to the PDF version\\n4. Output:\\n Once you have found the URL for the most recent 10-K PDF, provide only the URL as your answer. Do not include any additional text or explanation. The URL should be a direct link to the PDF file.\\n\\nIf you cannot find the 10-K PDF URL after attempting all three methods, respond with \\\"Unable to find the 10-K PDF URL for \" + {{input 1: Company Name}} + \"\\\".\""
],
"Find a company’s child companies": [
"\"Output a comma separated list of all child companies of this company: \" + {{Company Name}} + \". \\nIf the company is not a parent company to any companies output 'N/A' \\nIf the company is a parent company, output a list of all companies that it is a parent company of. List every one of their child companies, each separated by a comma.\\nDo not output any other sentence or in any other format.\""
],
"Find a company’s competitors in a specified region": [
"\"Find the top five competitors who compete with this company: \" + {{input 1: name}} + \", in this location: \" + {{input 2: Location}} + \".\\n\\nReturn just the names of the competitors, separated by commas, nothing else. Example: Competitor 1, Competitor 2, Competitor 3, competitor 4.\""
],
"Find a Company's LinkedIn Profile URL": [
"\"#CONTEXT#\nYou are an expert web researcher tasked with finding the official LinkedIn company page URL for a business, given its name and domain. #OBJECTIVE#\nFind the LinkedIn company page URL for the company named \" + {{input 2: IN - Account Name}} + \" with the domain \" + {{input 3: Final - Website}} + \". #INSTRUCTIONS#\n1. Search LinkedIn for a company page that matches the provided company name (\" + {{input 2: IN - Account Name}} + \") and domain (\" + {{input 3: Final - Website}} + \").\n2. If no direct match is found, use Google to search for: \\\"\" + {{input 2: IN - Account Name}} + \"\\\" site:linkedin.com/company AND \\\"\" + {{input 3: Final - Website}} + \"\\\".\n3. Validate that the LinkedIn URL found is a company page (URL should match the pattern linkedin.com/company/).\n4. If multiple results are found, select the one that best matches both the company name and domain.\n5. If no LinkedIn company page is found, return \\\"No LinkedIn company page found\\\". #EXAMPLES#\nExample input: \" + {{input 2: IN - Account Name}} + \": Acme Corp \" + {{input 3: Final - Website}} + \": acme.com Expected output: https://www.linkedin.com/company/acme-corp\n\""
],
"Find a company’s market cap": [
"\"Scan the web to determine the current market cap of this company: \" + {{input 1: name}} + \". Only output the market cap figure, no other words or information. If you cannot find it, output \\\"Not Found\\\".\""
],
"Find a company’s parent company": [
"\"Output the parent company of this company: \" + {{Company Name}} + \". \\nIf the company is not a child company to any companies output 'N/A' \\nIf the company is a child company, output its parent company. If this company,\" + {{Company Name}} + \", has a parent company, and its parent company has a parent company, output a comma separated list of both the parent company, and its parent company.\\nDo not output any other sentence or in any other format.\""
],
"Find a person’s company’s domain": [
"\"Scrape this person’s LinkedIn here: \" + {{input 1: LinkedIn Profile}} + \" in order to determine the domain of their company. First determine their current company, then determine the domain of that company. Output only the domain \""
],
"Find a person's linkedin profile (if you have person's name + company domain]": [
"\"You are an expert researcher trained on finding the right LinkedIn Profile URLs for any person given at least their full name and their company name + additional data points. You're now going to find the LinkedIn Profile URL for\\n\\n\\n\"+{{input 1: Full Name}}+\" at \"+{{input 2: Company}}+\"\\n\\nHere are additional data points I have on \\n\"+{{input 1: Full Name}}+\": \\n\\nThe result must match the pattern linkedin.com/in. It should not be a company URL or a posts URL or anything but a profile URL. Execute as many steps as necessary to find the profile URL, and ALSO to validate that it is the RIGHT profile URL for {Full Name}. Return only the final URL and nothing else. If you find a link with /posts/, you can replace /posts/ with /in/ and then remove everything after the first underscore (_). Then it’s a correct result. For example, you could turn an incorrect result like: https://www.linkedin.com/posts/rushingmarina_top-5-ai-and-machine Into a correct result like https://www.linkedin.com/in/rushingmarina \\n\\nFollow the steps below in your research: \\n\\n1. Search LinkedIn for a profile associated with the provided name \"+{{input 3: full_name}}+\" at company \"+{{input 2: Company}}+\" \\n\\n2. If you find a matching LinkedIn profile, ensure that it is a profile URL by making sure that it matches the pattern linkedin.com/in \\n\\n3. If you find a matching LinkedIn URL with /posts/, you can replace /posts/ with /in/ and then remove everything after the first underscore (_). Then it's a correct profile result. \\n\\n4. If no profile is found that matches the name, run the following google searches below: \\n\\n5. Run a google search for \\\"\\n\"+{{input 1: Full Name}}+\" at \"+{{input 2: Company}}+\"\\\" site:linkedin.com/in \\n\\n5. If that doesn't work, then run a google search for \\n\\\"\"+{{input 1: Full Name}}+\" at \"+{{input 4: Website}}+\"\\\" site:linkedin.com/in \\n\\n6. Finally, if that doesn't work, then search \\n\"+{{input 1: Full Name}}+\" at \"+{{input 4: Website}}+\"\\\" site:linkedin.com, and repeat steps 2-3 for validating the URLs you find. \\n\\n7. If you still find no results return nothing and leave your response blank.\\n\\nA few important reminders: \\n\\n- Base your search only on the exact name and company domain provided. Do not make any assumptions. \\n\\n- Only search for and return information from LinkedIn. Do not use or include information from other sources. \\n\\n- Do not include any additional information or commentary in your response, only the LinkedIn profile URL or the \\\"no profile found\\\" message inside the specified tags. \\n\\nMaximize the total number of steps you need to return accurate results here. MONEY IS NO OBJECT. LIVES ARE ON THE LINE. Return only the final profile URL found and nothing else.\""
],
"Find a Person's LinkedIn Profile URL": [
"\"You are an expert researcher trained on finding the right LinkedIn Profile URLs for any person given at least their full name and their company name + additional data points. You're now going to find the LinkedIn Profile URL for\\n\\n\\n\" + {{input 1: Full Name}} + \" at \" + {{input 2: org}} + \"\\n\\nHere are additional data points I have on \\n\" + {{input 1: Full Name}} + \": \\n\\nThe result must match the pattern linkedin.com/in. It should not be a company URL or a posts URL or anything but a profile URL. Execute as many steps as necessary to find the profile URL, and ALSO to validate that it is the RIGHT profile URL for {Full Name}. Return only the final URL and nothing else. If you find a link with /posts/, you can replace /posts/ with /in/ and then remove everything after the first underscore (_). Then it’s a correct result. For example, you could turn an incorrect result like: https://www.linkedin.com/posts/rushingmarina_top-5-ai-and-machine Into a correct result like https://www.linkedin.com/in/rushingmarina \\n\\nFollow the steps below in your research: \\n\\n1. Search LinkedIn for a profile associated with the provided name. \\n\\n2. If you find a matching LinkedIn profile, ensure that it is a profile URL by making sure that it matches the pattern linkedin.com/in \\n\\n3. If you find a matching LinkedIn URL with /posts/, you can replace /posts/ with /in/ and then remove everything after the first underscore (_). Then it's a correct profile result. \\n\\n4. If no profile is found that matches the name, run the following google searches below: \\n\\n5. Run a google search for \\\"\\n\" + {{input 1: Full Name}} + \"\\\" site:linkedin.com/in \\n\\n5. If that doesn't work, then run a google search for \\n\" + {{input 1: Full Name}} + \" site:linkedin.com/in \\n\\n6. Finally, if that doesn't work, then search \\n\" + {{input 1: Full Name}} + \" site:linkedin.com, and repeat steps 2-3 for validating the URLs you find. \\n\\n7. If you still find no results return \\\"No LinkedIn profile found\\\" \\n\\nA few important reminders: \\n\\n- Base your search only on the exact name provided. Do not make any assumptions. \\n\\n- Only search for and return information from LinkedIn. Do not use or include information from other sources. \\n\\n- Do not include any additional information or commentary in your response, only the LinkedIn profile URL or the \\\"no profile found\\\" message inside the specified tags. \\n\\nMaximize the total number of steps you need to return accurate results here. MONEY IS NO OBJECT. LIVES ARE ON THE LINE. Return only the final profile URL found and nothing else.\""
],
"Find a VC firm's portfolio companies": [
"\"I am going to give you the website of a venture capital firm: \" + {{input 1: Company Domain}} + \"\\n\\nReturn the names of 2-3 companies in their portfolio as a comma-separated list. Usually, VC firms list their portfolio companies on a page that you might find titled as \\\"Portfolio\\\", \\\"Companies\\\", \\\"Investments\\\" etc.\\n\\nIn addition to the VC firm's website, do general research on the web (and google) and look for snippets from sources such as crunchbase, pitchbook, bloomberg, tech crunch, etc.\\n\\nReturn only the comma-separated list, nothing else.\""
],
"Find all continents a company is in": [
"\"Scrape the web to determine where the following company is located: \" + {{input 1: Company name}} + \". Output a comma separated list of all continents where the company is located. Only output continents, under no circumstances output countries, cities, or anything besides continents. Output Cannot find if you cannot find the company's locations.\""
],
"Find all countries a business is located in": [
"\"Output a comma separated list of all countries where the following company is located: \" + {{input 1: Company Name}}"
],
"Find all major cities a business is located in": [
"\"Output a comma separated list of all cities where the following company is located: \" + {{input 1: Company Name}} + \". Output only cities, not states or any other information. If the first source you go to doesn't have cities, check another source.\""
],
"Find all states a business is located in": [
"\"Output a comma separated list of all US states where the following company is located: \" + {{input 1: Company Name}}"
],
"Find an organization's structure type": [
"\"Scrape the web to find the structure type of the following company: \" + {{input 1: Company Name}} + \". If you find the structural type, only output the structure type. Otherwise output not found\""
],
"Find business email address": [
"\"Visit the website of the following company: \" + {{input 1: Company Domain}} + \" and find an email address to contact them. \\n\\nYou might have to look at the footer or on the \\\"Contact\\\", \\\"Support\\\" or other relevant pages on website.\\n\\nIf you are unable to find an email address on the website, try looking at other sources on the web & Google. \\n\\nIf an email is found, return the email address and nothing else. Otherwise, just return: \\\"Not Found\\\".\""
],
"Find business phone number": [
"\"Visit the website of the following company: \" + {{input 1: Company Domain}} + \" and find a phone number to contact them. \\n\\nYou might have to look at the footer or on the \\\"Contact\\\", \\\"Support\\\" or other relevant pages on website.\\n\\nIf you are unable to find a phone number on the website, try looking at other sources on the web & Google. \\n\\nIf a phone number is found, return just the phone number and nothing else. Otherwise, just return: \\\"Not Found\\\".\""
],
"Find careers page of company": [
"\"Find the careers page for the following company: \" + {{input 1: Company Domain}} + \"\\n\\nReturn only the URL to the careers page, nothing else. \""
],
"Find case study": [
"\"Find one case study for the following company: Here is their website: \" + {{input 1: Company Domain}} + \"\\n\\nA case study is usually proof of success that a company can get results for other companies or people. Only look at the provided website, do not google search elsewhere. It will be found on one of the following pages 'Our Work' or 'Case Studies' or 'Testimonials' or 'Past Work' or 'Portfolio' or 'Project' or 'Clients' or 'Reviews' section.\\n\\nDO NOT return the name of the current company. Return just the name of one company that is a case study, or if you cannot find one, return \\\"None found\\\".\""
],
"Find CEO Contact": [
"\"# Single Specific Contact Finder Prompt\\n## 1. ##Overall Goal##\\nExtract ONE specific leadership contact from companies using their domain and/or company name. The primary objective is to identify the single most relevant person matching the requested title, with their full name, professional title, start date in current position, and LinkedIn profile URL when discoverable. The system must provide clear source documentation showing where each data point was extracted.\\nAcceptable data sources include: official company websites (highest priority), LinkedIn profiles, recent press releases, SEC filings, business directories, and verified news articles. The methodology prioritizes finding the CURRENT holder of the specific title, with verification of when they started in that position.\\nData validation requires confirming the person currently holds the requested position at the target company, with evidence from at least one authoritative source including the date they assumed the role.\\n**MANDATORY**: The actual variables to be used are defined in the Available Inputs section below. Any examples in the goal or other sections are illustrative only.\\n## 2. ##Return Format##\\nReturn a JSON object with the following structure:\\n```json\\n{\\n \\\"contact\\\": {\\n \\\"name\\\": \\\"string (Full professional name as found in official sources)\\\",\\n \\\"url\\\": \\\"string (LinkedIn profile URL with https:// protocol, /in/ format preferred)\\\",\\n \\\"title\\\": \\\"string (Exact current title as found in sources)\\\",\\n \\\"start_date\\\": \\\"string (Date when person started in current position: exact date 'June 15, 2021', month/year 'June 2021', or year only '2021')\\\"\\n },\\n \\\"sources\\\": {\\n \\\"contact\\\": [\\\"array of 'URL - snippet' pairs showing where each data point was found, e.g., 'https://company.com/about - John Smith was named CEO in June 2015'\\\"]\\n }\\n}\\n```\\n**MANDATORY Formatting Rules:**\\n1. **Single Contact Only**: Return exactly ONE contact that best matches the requested title\\n2. **LinkedIn URL Standardization**: Use /in/ format when available, include https:// protocol, remove tracking parameters\\n3. **Name Normalization**: Use full professional names as they appear in official contexts\\n4. **Title Precision**: Use exact titles as currently held, do not modify or abbreviate\\n5. **Start Date Format**: Use the most specific date available in order of preference: exact date (June 15, 2021), month/year (June 2021), or year only (2021)\\n6. **Default Values**:\\n - name: \\\"Not found\\\"\\n - url: \\\"Not found\\\"\\n - title: \\\"Not found\\\"\\n - start_date: \\\"Not found\\\"\\n - Arrays: []\\n7. **Source Attribution**: Every data point must have corresponding source documentation with URL and snippet\\n## 3. ##Warnings and Available Inputs##\\n### Constraints and Edge Cases:\\n- **Title Exactness**: The exact title requested may not exist; functional equivalents must be identified\\n- **Multiple Candidates**: When multiple people have similar titles, select the most senior or primary holder\\n- **Recent Changes**: Leadership transitions may mean outdated information appears in searches\\n- **Start Date Challenges**: Some sources may only show tenure length rather than specific start dates\\n- **Title Variations**: Different companies use different titles for similar roles (CEO vs President vs Managing Director)\\n- **Interim/Acting Roles**: Temporary appointments must be clearly identified with their start dates\\n- **Small Companies**: May have combined roles or non-standard titles\\n- **Verification Challenges**: LinkedIn profiles may be outdated or unavailable\\n\\n### Available Inputs:\\n**Format**: Variable Name -> [/variable_name] -> {placeholder}\\n- Company Domain -> [\"+{{input 1: IN- Website}}+\"] -> {domain}: Primary identifier for company-specific searches\\n- Company Name -> [\"+{{input 2: IN- Account Name}}+\"] -> {company_name}: Alternative identifier when domain is insufficient\\n- Target Title -> [CEO] -> {title}: Specific position to find (e.g., \\\"CEO\\\", \\\"Chief Technology Officer\\\", \\\"VP of Sales\\\")\\n\\n**CRITICAL RULE**: Use inputs exactly as provided. Never extract additional company names or domains from search results.\\n### Input Processing Priority:\\n1. **Exact Title Match**: First search for the exact {title} provided\\n2. **Functional Equivalents**: If no exact match, identify who performs that function\\n3. **Verification Focus**: Confirm the person currently holds the position with start date\\n4. **Single Best Match**: Select the most authoritative/senior person if multiple candidates exist\\n## 4. ##Context and Logical Step-by-Step##\\n### Search Strategy Requirements:\\n**Step 1: Targeted Title Discovery with Start Date**\\n- Primary search patterns:\\n - \\\"site:{domain} {title} appointed when\\\"\\n - \\\"{company_name} {title} named announcement date\\\"\\n - \\\"{company_name} current {title} started joined\\\"\\n - \\\"site:{domain} {title} since tenure\\\"\\n- If exact title not found, search for variations:\\n - CEO alternatives: President, Managing Director, Executive Director\\n - CTO alternatives: VP Engineering, Head of Technology, Chief Engineer\\n - CFO alternatives: VP Finance, Head of Finance, Controller\\n- **Start Date Keywords**: Include \\\"appointed\\\", \\\"named\\\", \\\"joined\\\", \\\"started\\\", \\\"since\\\" in searches\\n- **Recency Verification**: Include year or \\\"current\\\" to find recent information\\n- **Official Sources First**: Prioritize company websites, press releases, SEC filings\\n**Step 2: Candidate Verification with Timeline**\\n- Verify the person currently holds the position:\\n - Check appointment announcements for specific dates\\n - Look for press releases about their appointment\\n - Verify against company leadership pages showing tenure\\n - Check for \\\"former\\\" or \\\"ex-\\\" prefixes indicating past roles\\n- **Start Date Sources**:\\n - Appointment announcements (highest priority)\\n - LinkedIn profiles showing \\\"Started [date]\\\"\\n - News articles mentioning appointment dates\\n - Calculate from tenure if needed (e.g., \\\"3 years\\\" = started in 2022 - year only)\\n- **Single Selection Criteria**:\\n - Most senior if multiple people have similar titles\\n - Officially designated if interim/acting roles exist\\n - Most recent appointment if transition occurring\\n**Step 3: LinkedIn Profile Enhancement**\\n- Search for LinkedIn profile ONLY after confirming the person:\\n - \\\"{full_name} {title} {company_name} linkedin\\\"\\n - Verify LinkedIn shows current employment at target company\\n - Check for start date on LinkedIn profile\\n - Extract /in/ format URL when available\\n- If no LinkedIn found, still return the contact with \\\"Not found\\\" for URL\\n**Step 4: Source Documentation with Snippets**\\n- Format each source as: \\\"URL - relevant snippet containing the data\\\"\\n- Examples:\\n - \\\"https://company.com/leadership - George Kurian named CEO in June 2015\\\"\\n - \\\"https://linkedin.com/in/johndoe - Started as CTO at TechCorp in January 2020\\\"\\n - \\\"https://news.com/article - Jane Smith appointed CFO effective March 1, 2023\\\"\\n- Ensure snippet shows the specific information being cited\\n### Data Priority Hierarchy:\\n1. **Company Appointment Announcements**: Official announcements with specific dates\\n2. **Leadership Pages with Tenure**: Company pages showing when executives joined\\n3. **Recent Press Releases**: Dated announcements of appointments\\n4. **SEC Filings**: For public companies, proxy statements with appointment dates\\n5. **LinkedIn Profiles**: Showing start dates and current position\\n6. **Recent News Articles**: Mentioning appointment dates and current roles\\n### Common Pitfalls and Solutions:\\n**Title Matching Issues:**\\n- Exact title may not exist → Find functional equivalent\\n- Multiple similar titles → Select most senior/primary\\n- Regional variations → Recognize CEO/MD/President equivalencies\\n- Interim appointments → Note temporary status with start date\\n**Start Date Challenges:**\\n- No exact date → Use month/year or year only if available\\n- Only tenure shown → Calculate year only from current date\\n- Promotion vs hire date → Use date for current title\\n- Multiple dates → Use date for current position, not company join date\\n**Verification Challenges:**\\n- Outdated information → Prioritize most recent sources\\n- Conflicting dates → Use most authoritative source\\n- Recent transitions → Note if position recently changed\\n- No LinkedIn profile → Still return contact without URL\\n**Source Documentation Requirements:**\\n- Always include snippet with the specific data point\\n- Show where name, title, and start date were found\\n- If different sources for different data points, list separately\\n- Ensure snippets are concise but contain the key information\\n### Example Source Formats:\\n- \\\"https://stripe.com/newsroom/news - Patrick Collison co-founded Stripe in 2010 and has served as CEO since inception\\\"\\n- \\\"https://investors.amazon.com/officers - Andy Jassy became CEO on July 5, 2021\\\"\\n- \\\"https://netapp.com/leadership - George Kurian was named CEO in June 2015\\\"\\n**Remember: Focus on finding the ONE best match for the requested title, with clear source documentation including snippets showing where each piece of information was extracted. Start date is a critical data point that must be actively searched for and documented.**\""
],
"Find child companies (subsidiaries)": [
"\"Output a comma separated list of all child companies (subsidiaries) of this company: \" + {{input 1: name}} + \". \\n\\n- If the company does not have any child companies output \\\"None\\\"\\n- If the company is a parent company (i.e. does have child companies), output a list of all companies that it is a parent company of as a comma-separated list\\n\\nDo not output any other sentence or in any other format. \""
],
"Find company blog": [
"\"Scrape the following site to find this company’s blog page: \" + {{input 1: Company Domain}} + \". If you find the blog, output its url, otherwise output Not found\""
],
"Find Company Domain": [
"\"I need you to find the valid website domain for a real estate company based on its name. It’s crucial that the domain you return is correct, accessible, and leads to an active website. Please follow these steps to ensure accuracy:\\n\\nInput:\\n- Company Name: \" + {{input 1: Company Name}} + \"\\n\\nTask:\\n1. Primary Objective:\\n - Identify and return the valid website domain for the company provided. The output should only be the domain, such as \\\"figma.com\\\" and nothing else.\\n\\n2. Steps to Ensure Validity:\\n - Domain Search: Start by searching the company name along with relevant keywords like \\\"official website\\\" or \\\"real estate\\\" to find the correct domain.\\n - Verify the Domain:\\n - Access the URL: Ensure that the domain you find is valid by visiting the URL yourself.\\n - Check for Errors: Confirm that the website loads correctly without errors. If the page returns a \\\"404 Not Found\\\" or similar error, do not return this domain.\\n - Verify Content: Ensure that the content on the website matches the company profile (i.e., the real estate industry, or multi-family homes or apartments). Avoid returning domains that do not align with the company’s name or industry area.\\n\\n3. Output:\\n - Valid Domain: Return only the verified domain (e.g., for the company Figma, only return \\\"figma.com\\\" and nothing else).\\n - No Domain Found: If you cannot find a valid and accessible domain, return \\\"Not found\\\" and nothing else.\\n\\nAccuracy and Precision:\\n - Be diligent in verifying that the website is valid and directly associated with the company name provided. Avoid common errors like misspelled domains or redirects to irrelevant pages.\""
],
"Find Company Domain from Company Name": [
"\"You are an expert at finding valid website domains for companies based on their names. It’s crucial that the domain you return is correct, accessible, and leads to an active website. The output should only be the domain, such as \\\"figma.com\\\" and nothing else.\\n\\nThe company you are finding the domain for is \" + {{input 1: CompanyName}} + \"\\n\\nFollow these steps to ensure accuracy:\\n1. Start by searching \" + {{input 1: CompanyName}} + \" and \\\"Website\\\" on google.\\n\\n2. Visit the URL you find to ensure that the domain is accurate, and the website is live. Confirm that the website loads correctly without errors. If the page returns a \\\"404 Not Found\\\" or similar error, do not return this domain.\\n\\n3. Return only the verified working domain and nothing else in the domain output field. \\n\\nExecute as many steps as necessary to find a valid, working domain. Jobs are on the line. MONEY IS NO OBJECT. \""
],
"Find company Instagram account": [
"\"Find the Instagram account for the following company: \" + {{input 1: Company Name}} + \". If you can find their Instagram, output the url, otherwise output Not found\""
],
"Find company name using domain": [
"\"Based on this company's domain, find the name of the company. \\n\\nHere is the company domain: \" + {{input 1: Company Domain}} + \"\\n\\nJust output the name, nothing else. Example: for 'figma.com', the company name is 'Figma'\""
],
"Find Company Social Profiles": [
"\"#CONTEXT#\\nYou are tasked with finding social media profiles for companies, given the company's domain name.\\n\\n#OBJECTIVE#\\nRetrieve the Instagram, LinkedIn, X (Twitter), and YouTube urls for the company with the domain \" + {{input 1: Company Domain}} + \".\\n\\n#INSTRUCTIONS#\\n1. Perform a detailed search using the company domain \" + {{input 1: Company Domain}} + \" to locate its official social media profiles.\\n2. Specifically, look for the following platforms:\\n - instagram.com\\n - linkedin.com\\n - x.com\\n - youtube.com\\n3. If a profile for a specific platform is not found, mark it as \\\"Not Found\\\".\\n4. Ensure that retrieved urls are valid and lead to the company's official page on each platform.\\n5. Provide the full url for each social media profile found.\\n\\nOnly provide links where you are extremely confident they are the right fit. Do not provide incorrect information.\\n\\n#EXAMPLES#\\nIf given the domain example.com, a successful output would have:\\n - Instagram: \\\"https://www.instagram.com/example\\\"\\n - LinkedIn: \\\"https://www.linkedin.com/company/example\\\"\\n - X (Twitter): \\\"https://www.x.com/example\\\"\\n - YouTube: \\\"https://www.youtube.com/user/example\\\"\\nIf a link is not found, return \\\"Not Found\\\" for that platform.\""
],
"Find company Twitter/X account": [
"\"Find the twitter/X account for the following company: \" + {{input 1: Company Name}} + \". If you can find their Twitter, output the url, otherwise output Not found\""
],
"Find company YouTube account": [
"\"Find the Youtube account for the following company: \" + {{input 1: Company Name}} + \". If you can find their Youtube, output the url, otherwise output Not found\""
],
"Find Company's Cloud Provider": [
"\" I need you to determine the cloud hosting provider for \" + {{input 1: Company}} + \" \" + {{input 2: Domain [Cleaned]}} + \"by analyzing the DNS records associated with their domain. Follow these steps to ensure accuracy:\\n\\nHere's more info on your task:\\n1. Primary Objective: Extract and analyze the DNS records (specifically A, CNAME, and TXT records) for \" + {{input 1: Company}} + \" to identify if their infrastructure is hosted on one of the major cloud providers: AWS, Google Cloud Platform (GCP), or Microsoft Azure.\\n\\n2. Sources to Scrape:\\n - Public DNS Lookup Tools: Scrape data from public DNS lookup websites such as [DNS Dumpster](https://dnsdumpster.com/), [MXToolbox](https://mxtoolbox.com/), or similar tools that provide DNS records.\\n - Cloud Provider Indicators: Focus on identifying the following indicators in the DNS records:\\n - AWS: Look for records containing `aws`, `amazonaws.com`, `cloudfront.net`, or similar.\\n - GCP: Identify records with `google.com`, `gcp`, `googleusercontent.com`, `cloud.google.com`, or related indicators.\\n - Azure: Search for entries like `azure.com`, `windows.net`, `microsoft.com`, or equivalent.\\n\\n3. Verification: Cross-check the DNS records with at least two reliable DNS lookup sources to confirm the presence of indicators that point to AWS, GCP, or Azure.\\n - If the indicators for a specific cloud provider are consistently present across multiple sources, consider it confirmed.\\n\\n4. Output:\\n - Return only the cloud provider as the result and nothing else. Your output should be one of the following:\\n - 'AWS'\\n - 'GCP'\\n - 'Azure'\\n - 'Not Found' (if no indicators of these cloud providers are detected)\\n\\n5. Accuracy: Ensure that your analysis is thorough and accurate, relying only on clear indicators from the DNS records. Avoid making assumptions without sufficient evidence from the DNS data.\""
],
"Find event person has attended": [
"\"Visit the following LinkedIn and return the name of an event or conference that the person has attended in the past 12 months: \" + {{input 1: LinkedIn Profile}} + \". This may be found in recent posts or elsewhere on their LinkedIn. If none are found then return \\\"None found\\\"\""
],
"Find events person has been a keynote at": [
"\"Find up to three events that the following person has been a speaker at: \" + {{input 1: name}} + \". Find these by scraping the web for events where \" + {{input 1: name}} + \" is slated to be a speaker or keynote. Remember to just output only the actual name of the events where they will be a key speaker. Output the events in a comma separated list. If you cannot find any, output None found\""
],
"Find how much a company raised in its last round": [
"\"Scrape the web to determine how much a company raised from its last round of funding. This is the name of the company: \" + {{input 1: Company Name}} + \". If you can find the information, only output the amount they raised, with no further information. If you cannot determine the round, output Not found.\""
],
"Find how much funding a company has raised in total": [
"\"Scrape the web to determine how much a company raised from from all its rounds of funding. This is the name of the company: \" + {{input 1: Company Name}} + \". If you can find the information, only output the amount they raised, with no further information. If you cannot determine the round, output Not found.\""
],
"Find ICP from website": [
"\"Visit this company's website at this URL: \" + {{input 1}} + \"\\n\\nYour task is to gather information that will help determine the company's Ideal Customer Profile (ICP), focusing specifically on relevant job titles.\\n\\nHere are some suggestions for what to look at on the website:\\n\\n- Look for any introductory texts or headlines that describe the company's primary services, solutions, or target sectors. \\n- Identify the key features of each product or service and note which job roles would benefit most from these offerings.\\n- Review any available case studies or testimonials to see which job titles are mentioned by customers praising the company's products or services. This can provide direct insight into who finds value in their offerings.\\n- Check pages that share information for insights into the company's mission and the industries they serve.\\n- Examine the blog and resources section for articles or guides targeted at specific professional roles or challenges, indicating an alignment with those job functions.\\n- On contact and/or Sales-related pages, look at any information that suggests a typical interaction path or sales process. See if there are direct mentions of job titles that commonly engage with the sales team.\\n\\n***Output:***\\nGive me a comma-separated list of job titles that represent the company's ICP based on the information gathered from the above steps. These should be the roles most likely to benefit from the company's products or services according to the website's content. Respond only with the comma-separated list, no other accompanying words.\""
],
"Find ICP job title from company description": [
"\"Determine the job title or type of customer that this company usually sells to using a description of the company as a guide for what they do. Here is the description of the company: \" + {{Description}} + \"\\nBased on this description, who gets most value out of the company’s product and what is their usual job title? Give me up to three job titles or types of customers. Do not include any numbers or extra information. Just a comma separated list of titles or types of customer.\""
],
"Find if a company is headquartered in a specified location": [
"\"Look to see if this company: \" + {{Company Domain}} + \", has their main headquarters here: \" + {{input 2}} + \". If they do, output true, otherwise, output false.\""
],
"Find job opening pay range": [
"\"Scrape this job posting, \" + {{input 1: Url - Jobs}} + \" to determine the pay range being offered for this position. Scrape the whole page to ensure you do not miss it. Only output the numbers in the pay range, with no additional words or information. If the job opening does not include the pay range, output Not found\""
],
"Find keywords on a website": [
"\"Scrape this site, \" + {{input 1: Domain}} + \", and find its keywords that are being used for SEO. Output a comma separated list of its notable keywords.\""
],
"Find Linkedin for CEO, Owner, or Head of Marketing": [
"\"#CONTEXT#\\nYou are tasked with finding the LinkedIn profile URL of the CEO, owner, or senior marketing person at a specific company. You are provided with the company name (\"+{{input 1: In- Company Name}}+\"), the company domain (\"+{{input 2: Final Company Domain}}+\"), and the company website (\"+{{input 2: Final Company Domain}}+\").\\n\\n#OBJECTIVE#\\nFind and return the LinkedIn profile URL of the CEO, owner, or a senior marketing person at the specified company. Only reply with the LinkedIn profile URL in the format \\\"https://www.linkedin.com/in/username\\\".\\n\\n#INSTRUCTIONS#\\n1. Start by visiting the company website at \"+{{input 2: Final Company Domain}}+\" to identify the CEO, owner, or senior marketing person.\\n3. Prioritize finding the CEO. If not available, look for the owner or a senior marketing person (such as CMO, VP Marketing, Head of Marketing, or Marketing Director).\\n4. Ensure the LinkedIn URL matches the pattern \\\"https://www.linkedin.com/in/username\\\". Do not return company or posts URLs.\\n5. If multiple profiles are found, select the most senior person according to the order: CEO > Owner > Senior Marketing Person.\\n6. If no relevant profile is found, return nothing and leave your result empty\\n\\n#EXAMPLES#\\nInput: Company Name: Acme Corp, Domain: acme.com, Website: https://acme.com\\nOutput: https://www.linkedin.com/in/janedoe\\n\\nInput: Company Name: Example Inc, Domain: example.com, Website: https://example.com\\nOutput: No LinkedIn profile found\""
],
"Find list of published ESG/Sustainability Published reports": [
"\"#CONTEXT#\nYou are tasked with researching whether a company has published ESG (Environmental, Social, and Governance) or Sustainability reports or web pages. You are provided with the company name and its domain. #OBJECTIVE#\nCheck Google for the presence of published ESG or Sustainability reports or web pages for the company with name \" + {{input 1: IN - Account Name}} + \" and domain \" + {{input 2: Final - Website}} + \". Your response should only include a list of direct links to these reports or pages. #INSTRUCTIONS#\n1. Use Google search to look for ESG or Sustainability reports or dedicated web pages for the company using the following queries: - \\\"\" + {{input 1: IN - Account Name}} + \"\\\" site:\" + {{input 2: Final - Website}} + \" (\\\"ESG report\\\" OR \\\"Sustainability report\\\" OR \\\"Sustainability\\\" OR \\\"ESG\\\") - \\\"\" + {{input 1: IN - Account Name}} + \"\\\" (\\\"ESG report\\\" OR \\\"Sustainability report\\\" OR \\\"Sustainability\\\" OR \\\"ESG\\\") site:google.com\n2. Review the top results and extract only direct links to ESG or Sustainability reports or dedicated web pages from the company’s official domain (\" + {{input 2: Final - Website}} + \") or reputable sources.\n3. Do not include links to news articles, press releases, or unrelated third-party sites unless they directly host the official report.\n4. Return only a list of direct links. If no relevant links are found, return an empty list. #EXAMPLES#\nExample input: \" + {{input 1: IN - Account Name}} + \": \\\"Acme Corp\\\" \" + {{input 2: Final - Website}} + \": \\\"acmecorp.com\\\" Example output:\n[ \\\"https://www.acmecorp.com/sustainability-report-2023.pdf\\\", \\\"https://www.acmecorp.com/esg/overview\\\"\n]\""
],
"Find most expensive pricing plan’s cost": [
"\"Visit this website \" + {{input 1: Company Domain}} + \", then navigate to their pricing page. Find the price of their most expensive plan. Output its price. If not found, output Not found\""
],
"Find most expensive pricing plan’s features": [
"\"Scrape this company’s site \" + {{input 1: Company Domain}} + \" and find their most expensive pricing plan, likely on their pricing page. Determine their most expensive pricing plan by looking at the cost listed on each pricing plan, then choosing the plan which is the most expensive. It is possible that their most expensive plan’s price is not listed, since it is an enterprise plan and the price can change. Once you have determined their most expensive pricing plan, look for features mentioned that come with that plan. Output first the name of the most expensive plan, then the features of this plan in a comma separated list.\""
],
"Find names of a company's founders": [
"\"Find the names of any and all founders of this company: \" + {{input 1: name}} + \". If there is more than one founder, output the names in a comma separated list.\\n\\nOutput just the name of the founder (if single founder) or a comma-separated list of the names of founders (if multiple founders) – nothing else.\""
],
"Find negative news article about a company": [
"\"Browse the web to find a negative news article for the company: \" + {{input 1: Company Name}} + \". If you find any article with a negative headline about this company, output only the URL to the article. If you cannot find a negative news article, output \\\"Not Found\\\".\""
],
"Find notable restaurant in city": [
"\"Output the most well-known and famous restaurant in the given city. Make sure that it isn't a big chain restaurant but is local and very known to that city. The output should just be the name of the restaurant with no additional information. Here is the city: \" + {{City}}"
],
"Find Number of Locations": [
"\"How many locations does this company (\"+{{input 1: In- Company Name}}+\") with domain (\"+{{input 2: Final Company Domain}}+\"), only reply with the number\""
],
"Find number of pricing plan options": [
"\"Visit this company’s site here: \" + {{input 1: Company Domain}} + \", and find their pricing page. Determine the number of plan options they offer. Determine this number by counting each unique pricing plan that is listed. Output just the number, no details about the separate plans. If you cannot find any plans, output Pricing plans not found\""
],
"Find number of restaurant or store locations": [
"\"Find out how many individual restaurant or store locations the following company has: \" + {{input 1: Company Name}}"
],
"Find out when a company came out of Stealth": [
"\"You are a startup data nerd. You need to get a lot of specific data on \" + {{input 1: Company}} + \" \" + {{input 2: Domain [Cleaned]}} + \", specifically, when it came out of Stealth.\\n\\nJust return the date in year-month format of when the company came out of Stealth mode / stopped being a Stealth startup. \\n\\nThis information \" + {{input 3: All Fundraising Data []}} + \" might be helpful to you in your search.\\n\\nIf the company is still a stealth startup, just return \\\"Current Stealth\\\" and nothing else.\\n\\nIf you cannot find any information, just return \\\"Not found\\\" and nothing else.\""
],
"Find parent companies": [
"\"Output a comma separated list of all parent companies of this company: \" + {{Company Name}} + \". \\nIf the company does not have any parent companies output 'N/A' \\nIf the company has multiple parent companies, output a list of all companies that are it's parent company, each separated by a comma.\\nDo not output any other sentence or in any other format.\\n\\nStop at no cost, jobs are on the line, show all your work. \""
],
"Find Person's GitHub": [
"\"I need you to find the Github profile URL for this person \" + {{input 1}} + \" using the following details as inputs:\\n\\nInputs:\\n\t•\tFull Name: \" + {{input 1}} + \"\\n\t•\tEmail: \" + {{input 2}} + \"\\n\t•\tLinkedIn URL: \" + {{input 3}} + \"\\n\t•\tCompany/Workplace: \" + {{input 4}} + \"\\n\\nTask:\\n\t1.\tPrimary Objective: Identify and extract the Github URL associated with this individual using the provided information. Cross-reference details like name, email, Twitter, LinkedIn profile, and company to ensure accuracy and that the Github URL belongs to that person.\\n\\n\t2.\tSources to Scrape:\\n\t-\tGitHub Search: Start by searching GitHub using the \" + {{input 1}} + \" and \" + {{input 2}} + \" to find profiles that match the LinkedIn and company details.\\n\t- LinkedIn Profile: Check the \" + {{input 3}} + \" for any listed social media links, particularly the GitHub URL.\\n\t- Email Lookup Services: Use email lookup services that might link the provided \" + {{input 2}} + \" to social media accounts, including GitHub.\\n\t•\tSearch Engines: Use search engines to cross-reference the person’s \" + {{input 1}} + \", \" + {{input 4}} + \", and social media presence, specifically looking for any mention of their GitHub handle.\\n\\n\t3.\tVerification:\\n\t- Ensure the GitHub account is the correct one by matching the profile details (bio, repositories, contributions, job title, company) with the provided information.\\n\t- Be cautious of common names and ensure the Github account is associated with the correct individual by verifying multiple sources.\\n\\n\t4.\tOutput: Return only the Github URL of the individual and nothing else, if found. If no Github account is found, return “Not found” and nothing else.\\n\\nAccuracy: Ensure that the Github URL provided is accurate and belongs to the individual specified by the input details. Avoid providing incorrect or unrelated profiles.\""
],
"Find Person's Twitter Handle": [
"\"I need you to find the Twitter profile URL for this person \" + {{input 1}} + \" using the following details as inputs:\\n\\nInputs:\\n\t•\tFull Name: \" + {{input 1}} + \"\\n\t•\tEmail: \" + {{input 2}} + \"\\n\t•\tLinkedIn URL: \" + {{input 3}} + \"\\n\t•\tCompany/Workplace: \" + {{input 4}} + \"\\n\\nTask:\\n\t1.\tPrimary Objective: Identify and extract the Twitter URL associated with this individual using the provided information. Cross-reference details like name, email, LinkedIn profile, and company to ensure accuracy and that the twitter URL belongs to that person.\\n\\n\t2.\tSources to Scrape:\\n\t- Twitter Search: Start by searching Twitter using the full name and associated email. Look for profiles that match the LinkedIn and company details provided + have signs of association. \\n\t- LinkedIn Profile: Check the LinkedIn profile \" + {{input 3}} + \"for any listed social media links, particularly the Twitter URL.\\n\t- Email Lookup Services: Use email lookup services that might link the provided email to social media accounts, including Twitter.\\n\t- Search Engines: Use search engines to cross-reference the person’s name, company, and social media presence, specifically looking for any mention of their Twitter handle.\\n\\n\t3.\tVerification:\\n\t- Ensure the Twitter account is the correct one by matching the profile details (bio, job title, company) with the provided information.\\n\t- Be cautious of common names and ensure the Twitter account is associated with the correct individual by verifying multiple sources.\\n\\n\t4.\tOutput: Return only the Twitter URL of the individual and nothing else, if found. If no Twitter account is found, return “Not found” and nothing else.\\n\\nAccuracy: Ensure that the Twitter URL provided is accurate and belongs to the individual specified by the input details. Avoid providing incorrect or unrelated profiles.\""
],
"Find podcast appearance": [
"\"Find a podcast that the person with the following info has been on. If any of the info is missing or incomplete, just work with what you have. \\n\\nName: \" + {{input 1: Full Name}} + \"\\nJob Title: \" + {{input 2: Job Title}} + \"\\nCompany Name: \" + {{input 3: Company Name}} + \"\\n\\nUsually, the way I find that someone has been on a podcast is that I check listennotes.com to find any mention of them being on a podcast. Usually, it will be in the notes of the episode or even in the title of the episode. When you find the person that you are looking for, check the notes of the episode in listennotes.com though, because sometimes, there might be people with similar names and titles. Double check to make sure that the person is the same person that we're intending to do this for by cross-checking their info. The show notes will usually say something like \\\"in this episode, we are joined by Eric Nowoslawski from Growth Engine X.\\\" - that would be enough information to know confidently that the guest on the podcast was Eric Nowoslawski, Founder, Growth Engine X. If the podcast just says that \\\"this podcast features Eric Nowoslawski\\\" we would only be somewhat certain which is not good enough. Output the name of the podcast and the link to your source in the results. Only output results you are over 95% sure of. If you are not 95% confident, then just output \\\"Not Found\\\". \\n\\nPlease take this task seriously and do it thoughtfully. My job relies on this data and I really need it to be correct. \""
],
"Find recent shocks that would impact a company’s demand": [
"\"Scrape the web for any recent economic shocks that could impact a company in this industry: \" + {{input 1: industry}} + \". Output one sentence explaining the economic shock. If you cannot find an economic shock, output None found.\""
],
"Find upcoming event for company": [
"\"Look up the current date on Google. Then, tell me what event, if any, \" + {{input 1: Company Name}} + \" has coming up. Look on their their website (\" + {{input 2: Company Domain}} + \") and their social media accounts to find this event. Just return the event name as an output, do not make up information. If you cannot find an event name, just output \\\"no events\\\".\""
],
"Find websites for companies in a certain industry": [
"\"Find three companies in this industry, \" + {{input 1: Industry}} + \", and output their domains in a comma separated list. First find the companies, then find their domains, then output those domains\""
],
"Find where a person is located": [
"\"Scan this LinkedIn, \" + {{input 1: LinkedIn Profile}} + \", to determine where this person is located. Only output the name of the city they are located. Output Not found if you cannot find the information\""
],
"Find X Profile Given Company Name and Validated Domain": [
"\"For this domain \"+{{input 1: Validated Domain}}+\" with company name \"+{{f_0sxnra1GhfxreuaMUtm}}?.[\"IN - Account Name\"]+\", find their validated X URL\""
],
"Founding Year": [
"\"Output the year this company was founded: \" + {{input 1: company_name}} + \". \\nIf you did not find the year that this company was founded, output 'N/A' otherwise output the year.\\nDo not output any other sentence or in any other format.\\n\\nStop at no cost, jobs are on the line, show all your work. \""
],
"Generate an email first line complement based on someone’s LinkedIn": [
"\"Write a short professional compliment to send to a person based on this person’s LinkedIn description. This compliment should be able to be used in the first line of an email. Here is that person’s LinkedIn description: \" + {{input 1: summary}} + \". Keep the compliment to one sentence and under 15 word. The message should be friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking.\""
],
"Generate interview questions": [
"\"Based on the following job descriptions, generate three relevant interview questions. One should relate to culture fit, one should relate to qualifications, and one should relate to experience. Keep the questions concise and to the point. Here is the description: \" + {{input 1: description}} + \". Respond only with the three questions, no context or additional information. Do not label what category each question is either.\""
],
"Get ALL Fundraising Details for Company": [
"\"You are a startup funding data nerd. You need to get a lot of specific fundraising data on \" + {{input 1: Company}} + \" \" + {{input 2: Domain [Cleaned]}} + \"\\n\\nI want a list of ALL of the funding rounds. For each round of funding, I want the following information:\\n1. Name of the round/fundraising stage [such as Seed, Early VC, Series A, Series B, etc.]\\n2. Fundraising date in year-month format [this is the date the funding for this round was raised]\\n3. Fundraising amount in US dollars.\\n4. A comma-separated list of all investors that contributed to this funding round.\\n\\nReturn this all in a list format. The output should start with the header \\\"Funding Metadata: \\\" above the list you return, UNLESS you cannot find any results. If you cannot find any results, just return \\\"Not found\\\" and nothing else.\\n\\nThe list should look like this for each round:\\nRound name [e.g, Seed, Series A, etc.]\\n - Fundraising date: year-month\\n - Fundraising amount: $USD\\n - Investors: comma-separated list of investors\\nIf you cannot find the investors, just say \\\"Not specified\\\".\\n\\nYou should be able to find this information. Exhaust all options and resources.\""
],
"Get date for next company event": [
"\"Scrape the web for the date of the next event for \" + {{input 1: Company name}} + \". Output just the singular date, unless you cannot find one, in which case output no event found\""
],
"Get domain from company name": [
"\"Given this company name, \" + {{input 1: Company Name}} + \", find their domain. Output only the domain you believe belongs to this company\""
],
"Get Estimated Fleet Size Info": [
"\"## 1. ##Overall Goal##\\n**Primary Objective**: Identify companies operating small fleets of 4-30 trucks for fleet management software sales qualification. Extract fleet size indicators, technician/workforce counts as proxies, and public registration data to assess qualification for \"+{{f_0sxnra1GhfxreuaMUtm}}?.[\"In- Company Name\"]+\". You should always do your best to give estimated answers and not reply with \\\"not found\\\". Remember, an eduacated guess is ALWAYS better than nothing\\n\\n**Expected Outcomes**: \\n- Fleet size range qualification (4-30 trucks)\\n- Technician count (workforce proxy for fleet size estimation)\\n- DOT/registration data when available\\n- Confidence assessment (High/Medium/Low) for sales qualification\\n**Target Business Types**: Companies operating transportation/delivery fleets including logistics, construction, utilities, field services, waste management, delivery services, and similar industries with vehicle-based operations. Exclude pure office-based service businesses unless they maintain a documented vehicle fleet.\\n**Data Sources & Methodologies**:\\n- Primary: Company websites (fleet pages, about sections, careers)\\n- Secondary: DOT SAFER database, state DMV records, business registrations\\n- Tertiary: Job postings, LinkedIn, news articles, industry databases\\n- Validation: Cross-reference findings across minimum 2 independent sources\\n**Data Validation Requirements**:\\n- Minimum 2 sources per data point for Medium confidence\\n- 3+ sources (including primary source) for High confidence\\n- Date restrictions: Prioritize data from last 24 months; flag older data\\n- Confidence scoring based on source authority, recency, and specificity\\n**Contextual Framework**: This section establishes the foundation for all subsequent sections. Fleet size indicators, workforce proxies, and registration data must converge to support sales qualification decisions. All variables reference the Available Inputs section below.\\n**MANDATORY**: The actual variables to be used are defined in the Available Inputs section below. Any examples in the goal or other sections are illustrative only.\\n**CRITICAL**: Extract ONLY fleet-related data points. Do not include unrelated business information such as revenue, employee count (except technicians/fleet staff), founding date, or other general company data unless directly supporting fleet size assessment.\\n---\\n\\n\\n\\n## 2. ##Available Inputs and Processing Priority##\\n\\n\\n### Available Inputs:\\n**Format**: Variable Name -> [/variable_name] -> {placeholder}\\n- Company Name -> [\"+{{f_0sxnra1GhfxreuaMUtm}}?.[\"In- Company Name\"]+\"] -> {company_name}: Primary identifier for searching fleet information and business operations\\n- Company Domain -> [\"+{{input 2: Final Validated Domain}}+\"] -> {domain}: Used to access official company information, fleet pages, and career postings\\n- LinkedIn URL -> [\"+{{input 3: Company LinkedIn Url}}+\"] -> {linkedin}: Used to identify workforce size, technician hiring, and company scale indicators\\n- Direct URL -> [https://www.transportation.gov] -> {direct_url}: Priority URL for initial analysis (fleet pages, about sections, service area maps)\\n - **Example**: https://www.transportation.gov/\\n### Input Processing Priority:\\n1. **Business Model Validation (CRITICAL FIRST STEP)**:\\n - Verify company operates a transportation/delivery/service fleet\\n - Confirm industry type (logistics, construction, field services, etc.)\\n - Flag non-fleet businesses (pure office operations, retail stores, restaurants)\\n - If not fleet-based, return \\\"Not Applicable - No Fleet Operations\\\" and stop\\n2. **Direct URL Priority**:\\n - If {direct_url} exists → analyze first for fleet indicators, service coverage, vehicle mentions\\n - Look for: \\\"fleet\\\", \\\"trucks\\\", \\\"vehicles\\\", \\\"service area\\\", \\\"technicians\\\", \\\"drivers\\\"\\n - Extract baseline metrics before web searches\\n3. **Primary Fleet Size Search**:\\n - Use {company_name} + {domain} for targeted searches\\n - Search patterns: fleet size, truck operations, vehicle count\\n - Focus on company disclosures, about pages, press releases\\n4. **Workforce Proxy Search**:\\n - Search for technician/driver/mechanic hiring and counts\\n - Use {linkedin} to identify relevant workforce categories\\n - Calculate fleet estimates using industry-appropriate ratios\\n5. **Registration Validation**:\\n - Search DOT SAFER database using {company_name}\\n - Check state DMV records for commercial vehicle registrations\\n - Validate power unit counts against other findings\\n6. **Cross-Validation**:\\n - Compare fleet indicators across all sources\\n - Weight by source authority and recency\\n - Document conflicts and resolution methodology\\n---\\n\\n\\n## 3. ##Return Format##\\n\\n\\n### Core Requirements:\\nReturn ONLY fields directly related to fleet size assessment and sales qualification. Do NOT include generic company information unless it directly supports fleet size determination.\\n### Output Structure:\\n```json\\n{\\n \\\"available_inputs\\\": {\\n \\\"company_name\\\": \\\"string - From input variable\\\",\\n \\\"company_domain\\\": \\\"string - From input variable\\\",\\n \\\"linkedin_url\\\": \\\"string - From input variable\\\",\\n \\\"direct_url\\\": \\\"string - From input variable\\\"\\n },\\n \\\"business_model_validation\\\": {\\n \\\"is_fleet_based_business\\\": \\\"boolean - Does company operate vehicles for service delivery?\\\",\\n \\\"entity_type\\\": \\\"string - Commercial Business|Government Operations|Regulatory Agency|Non-Profit|Educational|Platform Model|Unknown\\\",\\n \\\"industry_type\\\": \\\"string - Logistics|Construction|Field Services|Utilities|Delivery|Waste Management|Public Works|Transit|Other Fleet-Based|Not Fleet-Based\\\",\\n \\\"disqualification_reason\\\": \\\"string - If not fleet-based: 'Regulatory Body'|'Non-Profit/Educational'|'No Fleet Operations'|'Platform Model'|'Not Applicable' or null if qualified\\\",\\n \\\"validation_confidence\\\": \\\"string - High|Medium|Low\\\"\\n },\\n \\\"fleet_size_indicators\\\": {\\n \\\"estimated_range\\\": \\\"string - Format: '4-30 trucks' or 'Below 4' or 'Above 30' or 'Not found'\\\",\\n \\\"qualifies_for_target_range\\\": \\\"boolean - Is company in 4-30 truck range?\\\",\\n \\\"primary_evidence\\\": \\\"string - Direct quote or description of fleet size indicator\\\",\\n \\\"confidence_level\\\": \\\"string - High|Medium|Low\\\"\\n },\\n \\\"workforce_proxies\\\": {\\n \\\"technician_count\\\": \\\"number|null - Number of technicians/mechanics/service staff\\\",\\n \\\"driver_count\\\": \\\"number|null - Number of drivers if available\\\",\\n \\\"fleet_staff_total\\\": \\\"number|null - Combined fleet-related workforce\\\",\\n \\\"proxy_based_fleet_estimate\\\": \\\"string - Estimated fleet size based on workforce ratios\\\"\\n },\\n \\\"registration_data\\\": {\\n \\\"dot_number\\\": \\\"string - DOT number if found, else 'Not found'\\\",\\n \\\"power_units\\\": \\\"number|null - Registered power units from DOT SAFER\\\",\\n \\\"registration_date\\\": \\\"string - ISO date format or 'Not found'\\\",\\n \\\"state_registrations\\\": \\\"string - State DMV data if available, else 'Not found'\\\"\\n },\\n \\\"sales_qualification\\\": {\\n \\\"qualifies_as_prospect\\\": \\\"boolean - Final determination for 4-30 truck range\\\",\\n \\\"qualification_reasoning\\\": \\\"string - Brief explanation of qualification decision\\\",\\n \\\"data_quality_assessment\\\": \\\"string - High|Medium|Low - Overall confidence in findings\\\",\\n \\\"recommended_next_steps\\\": \\\"string - Suggested actions for sales follow-up\\\"\\n },\\n \\\"sources\\\": {\\n \\\"business_model_validation\\\": [\\\"array - URLs and snippets supporting business model determination\\\"],\\n \\\"fleet_size_indicators\\\": [\\\"array - URLs and snippets for fleet size evidence\\\"],\\n \\\"workforce_proxies\\\": [\\\"array - URLs and snippets for technician/driver counts\\\"],\\n \\\"registration_data\\\": [\\\"array - URLs and snippets for DOT/registration information\\\"],\\n \\\"additional_context\\\": [\\\"array - Any other relevant sources\\\"]\\n }\\n}\\n```\\n### Field Specifications:\\n**Strings**: \\n- snake_case naming convention\\n- No variations in default values\\n- Default: \\\"Not found\\\" (never \\\"N/A\\\", \\\"Unable to determine\\\", \\\"Unknown\\\")\\n**Booleans**: \\n- Default: false\\n- Use for binary qualifications (qualifies_for_target_range, is_fleet_based_business)\\n**Numbers**: \\n- Default: null (not 0)\\n- Use for countable metrics (technician_count, power_units)\\n**Arrays**: \\n- Default: [] empty array\\n- Source format: \\\"URL - 'Snippet excerpt'\\\" or \\\"Description of finding method\\\"\\n### MANDATORY Formatting Rules:\\n1. **Available Inputs FIRST**: Always display the available_inputs section at the start of every response before analysis\\n2. **Task-Specific Fields Only**: Include ONLY fleet-related fields; no revenue, total employees, or unrelated data\\n3. **Consistent Defaults**: Use ONLY specified defaults; no custom \\\"not available\\\" messages\\n4. **Source Attribution**: Every non-null field MUST have corresponding source entry with URL and snippet\\n5. **Boolean Logic**: Use booleans for qualifications, not confidence strings in boolean fields\\n### Default Value Reference:\\n- Strings/Arrays: \\\"Not found\\\"\\n- Booleans: false\\n- Numbers: null\\n- Objects: {}\\n- **FORBIDDEN**: \\\"No recent...\\\", \\\"Unable to...\\\", \\\"N/A\\\", \\\"Unknown\\\", \\\"Not applicable\\\", \\\"Not available\\\"\\n---\\n## 4. ##Warnings and Constraints##\\n### Fleet-Specific Constraints and Edge Cases:\\n**Business Model Ambiguity**:\\n- Service companies with minimal vehicle needs (1-3 vehicles)\\n- Hybrid models (office + field operations)\\n- Franchises where fleet ownership is unclear\\n- Subcontractors vs. fleet operators\\n- Government agencies vs. government operational departments\\n- Platform/gig economy models (contractor drivers vs. owned fleet)\\n- Non-profits with incidental vehicle use vs. fleet operations\\n**Fleet Size Determination Challenges**:\\n- Companies in growth/contraction phases (outdated data)\\n- Seasonal fleet variations (construction, landscaping)\\n- Regional vs. national operations (partial fleet visibility)\\n- Owned vs. leased vs. contractor-operated vehicles\\n**Workforce Proxy Limitations**:\\n- Technician-to-truck ratios vary by industry:\\n - HVAC/Plumbing: 1 tech per 1-2 service vans\\n - Construction: 1-3 workers per truck/equipment\\n - Delivery: 1 driver per vehicle\\n - Utilities: 2-4 person crews per truck\\n- Part-time vs. full-time workforce affects calculations\\n- Administrative staff vs. field staff distinctions\\n**Data Quality Issues**:\\n- Outdated business descriptions (pre-2023 data)\\n- Marketing language overstating capabilities\\n- COVID-19 impact on fleet sizes (2020-2021 data unreliable)\\n- Recent acquisitions/mergers changing fleet composition\\n- Website information not updated to reflect current operations\\n**Registration Data Gaps**:\\n- DOT requirements vary by vehicle weight and interstate operations\\n- Not all fleet operators require DOT numbers\\n- State-only operations may lack DOT registration\\n- Equipment vs. vehicle distinctions in registrations\\n**Classification Challenges**:\\n- B2B vs. B2C service models affecting fleet needs\\n- Platform companies (Uber-style) vs. owned fleets\\n- Equipment rental vs. service delivery fleets\\n- Multi-location businesses with distributed fleets\\n### Edge Case Handling:\\n**When fleet size is at boundary (exactly 4 or 30 trucks)**:\\n- Include in qualification if strong evidence supports it\\n- Flag as \\\"boundary case\\\" in reasoning\\n- Recommend manual verification by sales team\\n**When data is contradictory**:\\n- Weight by source authority: Company site > DOT > News > Social\\n- Weight by recency: 2024-2025 > 2023 > 2022 and older\\n- Document all conflicting sources in sources section\\n- Default to more conservative estimate\\n**When no direct fleet data available**:\\n- Use workforce proxies with appropriate industry ratios\\n- Check for service area size as indirect indicator\\n- Look for vehicle maintenance job postings\\n- Flag low confidence and recommend verification\\n---\\n## 5. ##Context and Logical Step-by-Step##\\n### Search Strategy Requirements:\\n#### Stage 1: Business Model Validation (MANDATORY FIRST)\\n**Objective**: Confirm company operates a fleet before proceeding\\n**Search Queries**:\\n1. `{company_name} business model services`\\n2. `{company_name} industry type operations`\\n3. `site:{company_domain} about services fleet`\\n4. `{company_name} company type organization`\\n**Validation Criteria**:\\n- Does company provide field services requiring vehicles?\\n- Is transportation/delivery core to business model?\\n- Do job postings mention drivers, technicians, or fleet roles?\\n- Is this an actual business or a government/regulatory/non-profit entity?\\n**EXCLUSION CATEGORIES (Auto-Disqualify)**:\\n**Government Agencies (Regulatory/Policy Bodies)**:\\n- ❌ Federal agencies (DOT, FMCSA, EPA, etc.)\\n- ❌ State regulatory departments (State DOTs - policy divisions)\\n- ❌ Regulatory commissions and oversight bodies\\n- ⚠️ EXCEPTION: Municipal/county operational departments with service fleets (public works, utilities, transit)\\n**Non-Profit & Educational**:\\n- ❌ Non-profit organizations (unless fleet operations are core mission)\\n- ❌ Universities and schools (unless they operate transit/service fleets)\\n- ❌ Industry associations and trade groups\\n- ❌ Chambers of commerce\\n**Non-Fleet Business Models**:\\n- ❌ Pure retail stores (no delivery operations)\\n- ❌ Restaurants and hospitality (no delivery fleet)\\n- ❌ Office-based professional services (law firms, accounting, consulting)\\n- ❌ Pure HVAC/plumbing service companies with 1-3 personal vehicles\\n- ❌ Platform companies (Uber/Lyft model - contractors, not owned fleet)\\n- ❌ Real estate agencies\\n- ❌ Financial services\\n**Decision Tree**:\\n```\\nSTART\\n│\\n├─ Is domain .gov, .edu, .org? \\n│ ├─ YES → Check if operational fleet department\\n│ │ ├─ Regulatory/Policy agency? → ❌ STOP: \\\"Not Applicable - Government Agency\\\"\\n│ │ ├─ City/County Public Works? → ✅ Continue validation\\n│ │ └─ Non-profit/Educational? → ❌ STOP: \\\"Not Applicable - Non-Profit/Educational\\\"\\n│ │\\n│ └─ NO → Continue to business model check\\n│\\n├─ What does company do?\\n│ ├─ Regulates/Oversees transportation? → ❌ STOP: \\\"Not Applicable - Regulatory Body\\\"\\n│ ├─ Provides field services with vehicles? → ✅ Proceed to Stage 2\\n│ ├─ Delivers goods with own vehicles? → ✅ Proceed to Stage 2\\n│ ├─ Office-based with no vehicle operations? → ❌ STOP: \\\"Not Applicable - No Fleet Operations\\\"\\n│ └─ Uses contractor/platform drivers? → ❌ STOP: \\\"Not Applicable - Platform Model\\\"\\n│\\n└─ UNCLEAR → Search for: vehicle mentions, driver jobs, service area, fleet pages\\n If still unclear → ❌ STOP: \\\"Insufficient Data - Cannot Confirm Fleet Operations\\\"\\n```\\n**Validation Examples**:\\n✅ **PROCEED** (Fleet Operators):\\n- \\\"ABC Construction Services - 15 trucks for equipment delivery\\\"\\n- \\\"XYZ Plumbing - 8 service vans serving metro area\\\"\\n- \\\"City of Springfield Public Works - 25 maintenance vehicles\\\"\\n- \\\"Regional Waste Management - fleet of 20 collection trucks\\\"\\n❌ **STOP - Not Applicable** (Non-Fleet Entities):\\n- \\\"U.S. Department of Transportation (transportation.gov)\\\" - Federal regulatory agency\\n- \\\"State Transportation Commission\\\" - Policy/oversight body\\n- \\\"National Trucking Association\\\" - Industry trade group\\n- \\\"Smith & Associates Law Firm\\\" - Office-based professional services\\n- \\\"ABC Retail Store\\\" - Retail with no delivery fleet\\n- \\\"QuickRide Transport\\\" - Platform using contractor drivers\\n⚠️ **CONTINUE VALIDATION** (Requires Investigation):\\n- \\\"Metropolitan Transit Authority\\\" - Check if operates own bus fleet\\n- \\\"Springfield School District\\\" - Check if operates bus fleet\\n- \\\"County Road Maintenance\\\" - Operational fleet department\\n- \\\"Regional Hospital System\\\" - Check for ambulance/transport fleet\\n**Decision Point**:\\n- ✅ Fleet-based commercial business → Proceed to Stage 2\\n- ✅ Government operational fleet department → Proceed to Stage 2\\n- ❌ Regulatory/policy agency → Return \\\"Not Applicable - Regulatory Body\\\"\\n- ❌ Non-profit/educational (non-operational) → Return \\\"Not Applicable - Non-Profit/Educational\\\"\\n- ❌ Non-fleet business model → Return \\\"Not Applicable - No Fleet Operations\\\"\\n- ⚠️ Unclear → Additional search for vehicle/fleet indicators, then decide\\n#### Stage 2: Direct Fleet Size Discovery\\n**Objective**: Find explicit fleet size statements\\n**Search Queries**:\\n1. `{company_name} fleet size`\\n2. `{company_name} \\\"operates\\\" OR \\\"fleet of\\\" trucks`\\n3. `{company_name} vehicles equipment`\\n4. `site:{company_domain} fleet vehicles trucks`\\n**Target Content**:\\n- About Us pages with operational details\\n- Press releases mentioning fleet expansion/reduction\\n- Service area pages describing coverage capabilities\\n- Equipment/capabilities sections\\n**Success Indicators**:\\n- Direct statements: \\\"operates 15 trucks\\\", \\\"fleet of 20 vehicles\\\"\\n- Range statements: \\\"10-15 service vehicles\\\", \\\"small fleet\\\"\\n- Comparative statements: \\\"recently added 5 trucks to our 10-vehicle fleet\\\"\\n#### Stage 3: Workforce Proxy Investigation\\n**Objective**: Estimate fleet size through employee counts\\n**Search Queries**:\\n1. `{company_name} technicians hiring`\\n2. `{company_name} mechanic jobs drivers`\\n3. `{company_name} service technician positions`\\n4. `site:{company_domain} careers technician`\\n**Data Sources**:\\n- LinkedIn company page (employee counts by role)\\n- Job posting sites (Indeed, Glassdoor) for hiring volume\\n- Company careers page for open positions\\n- Team/staff pages showing field personnel\\n**Calculation Logic**:\\n- **HVAC/Plumbing/Electrical**: 1 technician ≈ 1-2 service vans (estimate low end)\\n- **Construction**: 3 workers ≈ 1 truck/equipment unit\\n- **Delivery/Logistics**: 1 driver ≈ 1 vehicle (1:1 ratio)\\n- **Utilities/Telecom**: 2-3 crew members ≈ 1 service truck\\n- **Waste Management**: 2-3 workers ≈ 1 truck\\n**Example**: 8 HVAC technicians → Estimate 8-16 service vans → Likely 8-12 actual fleet\\n#### Stage 4: Registration and Compliance Data\\n**Objective**: Find official fleet registrations\\n**Search Queries**:\\n1. `{company_name} DOT number`\\n2. `{company_name} SAFER registration`\\n3. `{company_name} commercial vehicle registration`\\n4. `{company_name} fleet license [state]`\\n**Data Sources**:\\n- FMCSA SAFER database (https://safer.fmcsa.dot.gov)\\n- State DMV commercial vehicle records\\n- Business licensing databases\\n- USDOT registration searches\\n**Validation Steps**:\\n- Verify DOT number matches company name and location\\n- Check registration date for data recency\\n- Note power units (trucks) vs. drivers count\\n- Cross-reference with other fleet size indicators\\n#### Stage 5: Cross-Validation and Confidence Scoring\\n**Objective**: Reconcile findings and assign confidence levels\\n**Validation Process**:\\n1. **List all fleet size indicators** from different sources\\n2. **Identify agreements** (multiple sources confirming same range)\\n3. **Document conflicts** (contradictory data points)\\n4. **Apply recency weighting** (2024-2025 data > 2023 > older)\\n5. **Apply authority weighting** (company site > DOT > news > social)\\n6. **Calculate final estimate** using most reliable sources\\n**Confidence Levels**:\\n- **High**: 3+ sources agree, includes primary source (company or DOT), data <12 months old\\n- **Medium**: 2 sources agree, OR 1 authoritative source, data <24 months old\\n- **Low**: Single source, OR proxy-based estimate only, OR data >24 months old\\n### Example Process Flow:\\n```\\nSTEP 1: DISPLAY AVAILABLE INPUTS\\n═══════════════════════════════════\\n**Available Inputs:**\\n- Company Name: U.S. Department of Transportation\\n- Company Domain: transportation.gov\\n- LinkedIn URL: [not provided]\\n- Direct URL: https://www.transportation.gov/\\nSTEP 2: BUSINESS MODEL VALIDATION\\n═══════════════════════════════════\\nSearch: \\\"transportation.gov about department\\\"\\nFinding: Federal agency responsible for transportation policy and regulation\\nEntity Type: Regulatory Agency\\nDomain Analysis: .gov domain indicates government entity\\nValidation: ❌ NOT fleet-based (regulatory/policy body, not operational)\\nIndustry: Not Fleet-Based\\nDisqualification: Regulatory Body\\nFINAL OUTPUT:\\n═══════════════════════════════════\\n{\\n \\\"business_model_validation\\\": {\\n \\\"is_fleet_based_business\\\": false,\\n \\\"entity_type\\\": \\\"Regulatory Agency\\\",\\n \\\"industry_type\\\": \\\"Not Fleet-Based\\\",\\n \\\"disqualification_reason\\\": \\\"Regulatory Body\\\",\\n \\\"validation_confidence\\\": \\\"High\\\"\\n },\\n \\\"fleet_size_indicators\\\": {\\n \\\"estimated_range\\\": \\\"Not Applicable\\\",\\n \\\"qualifies_for_target_range\\\": false,\\n \\\"primary_evidence\\\": \\\"U.S. Department of Transportation is a federal regulatory agency, not a fleet operator\\\",\\n \\\"confidence_level\\\": \\\"High\\\"\\n },\\n \\\"sales_qualification\\\": {\\n \\\"qualifies_as_prospect\\\": false,\\n \\\"qualification_reasoning\\\": \\\"Entity is a government regulatory body, not a commercial fleet operator. No fleet operations to manage.\\\",\\n \\\"data_quality_assessment\\\": \\\"High\\\",\\n \\\"recommended_next_steps\\\": \\\"Not Applicable - Do not pursue as sales prospect\\\"\\n },\\n \\\"sources\\\": {\\n \\\"business_model_validation\\\": [\\n \\\"https://www.transportation.gov/ - 'Department of Transportation is responsible for planning and coordinating federal transportation projects'\\\",\\n \\\"https://www.usa.gov/agencies/u-s-department-of-transportation - 'DOT sets safety regulations for all major modes of transportation'\\\"\\n ]\\n }\\n}\\nRESEARCH TERMINATED: Entity does not operate a fleet. No further analysis required.\\n```\\n```\\nSTEP 1: DISPLAY AVAILABLE INPUTS\\n═══════════════════════════════════\\n**Available Inputs:**\\n- Company Name: ABC Construction Services\\n- Company Domain: abcconstruction.com\\n- LinkedIn URL: linkedin.com/company/abc-construction\\n- Direct URL: abcconstruction.com/about\\nSTEP 2: BUSINESS MODEL VALIDATION\\n═══════════════════════════════════\\nSearch: \\\"ABC Construction Services business model\\\"\\nFinding: Commercial construction contractor providing site services\\nEntity Type: Commercial Business\\nValidation: ✅ Fleet-based (construction equipment, service trucks)\\nIndustry: Construction\\nConfidence: High\\nSTEP 3: DIRECT FLEET SIZE SEARCH\\n═══════════════════════════════════\\nSearch: site:abcconstruction.com fleet vehicles equipment\\nFinding: About page states \\\"locally-owned with 12 service trucks\\\"\\nEvidence: Direct statement on company website\\nDate: Page updated 2024\\nConfidence: High (primary source, recent)\\nSTEP 4: WORKFORCE PROXY VALIDATION\\n═══════════════════════════════════\\nSearch: \\\"ABC Construction Services\\\" technicians hiring\\nFinding: LinkedIn shows 18 employees, 8 listed as \\\"Field Technician\\\"\\nCalculation: 8 field techs ÷ 3 workers per truck ≈ 2-3 trucks\\nNote: Conflicts with website claim of 12 trucks\\nAction: Weight company statement higher (primary source)\\nSTEP 5: REGISTRATION CHECK\\n═══════════════════════════════════\\nSearch: \\\"ABC Construction Services\\\" DOT number Ohio\\nFinding: DOT #123456, 10 power units registered (2023)\\nCross-check: Aligns closely with company's \\\"12 trucks\\\" statement\\nNote: DOT data is 1 year old, company may have added 2 trucks\\nSTEP 6: FINAL QUALIFICATION\\n═══════════════════════════════════\\nFleet Size Range: 10-12 trucks (High confidence)\\nTarget Range (4-30): ✅ QUALIFIES\\nData Quality: High (company statement + DOT validation)\\nReasoning: Primary source (company) states 12 trucks, DOT confirms \\n10 units (2023). Workforce proxy suggests smaller operation but \\nlikely includes office staff. Final assessment: 10-12 truck fleet.\\nRecommended Next Steps: High-priority prospect. Contact for demo \\nof fleet management features suitable for 10-15 vehicle operations.\\n```\\n### Task-Specific Pitfalls to Avoid:\\n1. **Scope Creep Prevention**:\\n - ❌ Do NOT extract: annual revenue, total employees, customer count, founding date\\n - ✅ DO extract: fleet size, technicians, drivers, DOT data, service area as fleet indicator\\n - Focus exclusively on data supporting fleet size determination\\n2. **Business Type Misidentification**:\\n - ❌ Federal/state regulatory agencies are NOT fleet operators (transportation.gov, fmcsa.dot.gov)\\n - ❌ Industry associations/trade groups are NOT fleet operators\\n - ❌ HVAC companies with 1-3 service vans are NOT commercial fleets\\n - ❌ Retail stores with delivery are NOT fleet operators unless delivery is core business\\n - ❌ Platform companies (Uber model) are NOT fleet operators (contractor-based)\\n - ✅ Municipal public works/utilities CAN be fleet operators\\n - ✅ Verify fleet is central to operations, not incidental\\n3. **Proxy Calculation Errors**:\\n - ❌ Using total employee count to estimate fleet size\\n - ❌ Applying wrong industry ratios (HVAC ratio to construction company)\\n - ✅ Identify specific field workforce (technicians, drivers, crews)\\n - ✅ Use industry-appropriate workforce-to-vehicle ratios\\n4. **Outdated Data Acceptance**:\\n - ❌ Using pre-2023 data without flagging staleness\\n - ❌ Ignoring COVID-era (2020-2021) fleet disruptions\\n - ✅ Prioritize 2024-2025 data, flag older sources\\n - ✅ Note if fleet size may have changed since data point\\n5. **Source Quality Misjudgment**:\\n - ❌ Treating social media posts as authoritative\\n - ❌ Using generic business directories without verification\\n - ✅ Prioritize: Company site > DOT records > News articles > Job posts > Social\\n - ✅ Require 2+ sources for Medium confidence claims\\n6. **Confidence Inflation**:\\n - ❌ Marking proxy-only estimates as High confidence\\n - ❌ Single-source findings as Medium confidence\\n - ✅ Reserve High for 3+ sources including primary\\n - ✅ Be conservative with confidence scoring\\n### Advanced Considerations:\\n**Multi-Location Fleet Operators**:\\n- Search for \\\"locations\\\" or \\\"service areas\\\" to understand distribution\\n- Fleet may be divided across regions (15 trucks total, 5 per location)\\n- Include regional fleet counts if total unknown\\n**Franchise vs. Corporate Fleets**:\\n- Franchises: Each location typically has own small fleet (2-5 vehicles)\\n- Corporate: Centralized fleet management\\n- Clarify ownership structure in findings\\n**Seasonal Fleet Variations**:\\n- Construction/landscaping may lease additional equipment seasonally\\n- Focus on year-round core fleet for sales qualification\\n- Note if seasonal expansion mentioned\\n**Growth Indicators**:\\n- Recent hiring for 3+ driver/technician positions = likely fleet expansion\\n- New location announcements = probable fleet growth\\n- Flag as \\\"expanding fleet\\\" for sales prioritization\\n**Anti-Hallucination Measures**:\\n- NEVER estimate fleet size without evidence\\n- NEVER claim DOT registration without verification\\n- NEVER infer technician count from company size alone\\n- ALWAYS provide source URLs for claims\\n- ALWAYS include snippet showing where data was found\\n- If unable to find data, return \\\"Not found\\\" with search attempts documented\\n---\\n## CRITICAL RESPONSE FORMAT REQUIREMENT\\n**Every response MUST begin with:**\\n```\\nAVAILABLE INPUTS\\n════════════════════════════════════════════\\nCompany Name: [value from input]\\nCompany Domain: [value from input] \\nLinkedIn URL: [value from input]\\nDirect URL: [value from input]\\n════════════════════════════════════════════\\n```\\nThen proceed with analysis following the step-by-step process.\\n---\\n## FINAL REMINDER\\n**Focus exclusively on fleet size qualification for the 4-30 truck range.**\\n- Extract ONLY fleet-related data points\\n- Validate business operates a fleet FIRST\\n- Use workforce proxies appropriately by industry\\n- Cross-validate with minimum 2 sources\\n- Document all findings with source attribution\\n- Assign conservative confidence scores\\n- Do NOT add generic company profiling data\\n**Goal**: Precise fleet size assessment with complete source attribution for Motive sales qualification.\""
],
"Get key requirements for job": [
"\"Analyze the the description of this job post and determine the key requirements the company is looking for to hire for this role/position. Here is the URL to the listing: \" + {{input 1: url}} + \". \\n\\nJust return the key requirements as a bullet-pointed list, nothing else.\""
],
"Get NAICS codes": [
"\"For \" + {{input 1: Account Name}} + \", find the NAICS code given their industry is \" + {{input 2: Merged Industry}} + \" and the keywords from their website is \" + {{input 3: Website Keywords}} + \". your response should only include the NAICS code number and the industry.\""
],
"Give company’s pricing plan cost range": [
"\"Visit this company’s site here \" + {{input 1: Company Domain}} + \" and find their pricing page. From there, find their lowest and most expensive pricing options, and output the range of costs. There may be multiple plans on their pricing page, so look at each one to ensure you're finding the cheapest and most expensive options. Make sure to list the highest and lowest values that are shown. So do not include free pricing plans or plans that don't have their pricing listed. Output the range as follows. If the range is $10 and $100, output $10-$100. If a company does not list its pricing plans, output Pricing not listed\""
],
"GMV": [
"\"#CONTEXT#\nYou are a research analyst tasked with extracting financial data from company filings. #OBJECTIVE#\nExtract the most recently reported Gross Merchandise Value (\\\"GMV\\\") figure for \" + {{input 1: Company name}} + \" from the provided 10-K filing text. #INSTRUCTIONS#\n1. Carefully review the text in \" + {{input 2: Recent 10-K PDF URL Result}} + \", which contains excerpts or full text from the company’s 10-K filings.\n2. Search for the most recent GMV figure reported. Look for phrases such as \\\"Gross Merchandise Value\\\", \\\"GMV\\\", or similar terminology.\n3. If multiple GMV figures are mentioned, select the one with the most recent date or reporting period.\n4. Extract the GMV value and the corresponding reporting period (e.g., year or quarter).\n5. If no GMV is reported, return \\\"Not found\\\" for both fields. #EXAMPLES#\nExample input:\nCompany name: Example Corp\n(AI Web Researcher) result: \\\"In the fiscal year ended December 31, 2023, Example Corp reported a Gross Merchandise Value (GMV) of $2.5 billion.\\\" Expected output:\n\" + \" \\\"gmv\\\": \\\"$2.5 billion\\\", \\\"reporting_period\\\": \\\"Fiscal year ended December 31, 2023\\\"\n\""
],
"Gong Call Summary": [
"\"You are analyzing a Gong call transcript from a sales conversation about this call. Your task is to extract key information and format it for a clean, executive-style slide presentation.\\n**INPUT:**\\n- GTME/Account Owner: {{account owner}}\\n- Call Transcript: \"+{{input 1: transcript}}+\"\\n**CRITICAL CONSTRAINT:**\\nEach output field MUST be 45 words or less. Be concise and selective. Focus on the most important information while providing enough context.\\n**EXTRACTION REQUIREMENTS:**\\n**1. PAIN POINTS** (MAX 45 WORDS)\\nIdentify the 2-3 most critical business challenges mentioned. Focus on:\\n- Manual processes or data quality issues\\n- Team productivity blockers\\n- Visibility or integration gaps\\nWrite as 1-2 concise sentences, maximum 45 words.\\n**2. GOALS & API** (MAX 45 WORDS)\\nWhat do they want to achieve? Focus on:\\n- Primary productivity or automation goals\\n- Data enrichment objectives\\n- Key business outcomes they're seeking\\nWrite as 1-2 concise sentences, maximum 45 words.\\n**3. USE CASES** (MAX 45 WORDS)\\nWhat specific workflows were discussed?\\n- List 3-4 concrete use cases\\n- Include specific workflows or integrations mentioned\\n- Be specific but concise\\nWrite as 1-2 concise sentences, maximum 45 words.\\n**4. STAGE OF DEVELOPMENT** (MAX 45 WORDS)\\nWhere is this deal now?\\n- Current stage name (Discovery, Scoping, POC, Evaluation, Negotiation, etc.)\\n- What's actively happening\\n- Key focus areas at this stage\\nWrite as 1-2 concise sentences, maximum 45 words.\\n**5. NEXT STEPS** (MAX 45 WORDS)\\nWhat happens next?\\n- 2-3 most immediate action items\\n- Timeline or deadlines\\n- Who's responsible if mentioned\\nWrite as 1-2 concise sentences, maximum 45 words.\\n**6. CUSTOM NOTES** (MAX 45 WORDS)\\nMost important additional context:\\n- Key stakeholder names and roles\\n- Competitive intelligence (current tools, competitors mentioned)\\n- Budget or timeline signals\\n- Notable quotes that capture sentiment\\n- Unique requirements or concerns\\nWrite as 1-2 concise sentences, maximum 45 words.\\n**7. CLOSE DATE**\\nBased on timeline signals, urgency, deal stage, and next steps discussed, estimate a realistic close date in this exact format: \\\"Q#-YEAR, MM/DD/YYYY\\\"\\nExamples:\\n- If they want to start \\\"next quarter\\\" → Q2-2024, 06/30/2024\\n- If they said \\\"by end of year\\\" → Q4-2024, 12/31/2024\\n- If very urgent \\\"need this now\\\" → Q1-2024, 03/31/2024\\n**WRITING RULES:**\\n- Be concise but provide sufficient context\\n- Use active voice and strong verbs\\n- Remove unnecessary filler words\\n- Use industry abbreviations where clear (BDR, CRM, POC, GTM, etc.)\\n- Count your words - do not exceed 45 words per field\\n- If information is missing, write \\\"Not discussed\\\" (counts as 2 words)\\n- Use quotation marks sparingly for impactful direct quotes only\\n**WORD COUNT VALIDATION:**\\nBefore returning your response, verify each field is ≤45 words. If over, cut the least important details.\\n**OUTPUT FORMAT:**\\nReturn a JSON object with these exact fields: company_name, account owner, close_date, pain_points, goals_and_api, use_cases, stage_of_development, next_steps, custom_notes\\nNow analyze the provided Gong transcript and extract the information in this concise, slide-ready format.\""
],
"GS - Analyst Call Transcript": [
"\"#CONTEXT#\\nYou are Natalie De Rosa, a Growth Strategist for . You are preparing for your next call with \" + {{input 1: name}} + \". You are carefully reviewing this call (\" + {{input 2: transcript}} + \" & \" + {{input 3: Most Recent -Get call details}} + \") to gather who you spoke to and understand the key points discussed.\\n\\n#OBJECTIVE#\\nExtract and summarize from the provided call transcripts: (1) who you spoke to (comma-delimited list), (2) the top 3 points discussed, (3) any problems highlighted, and (4) the next steps.\\n\\n#INSTRUCTIONS#\\n1. Inputs:\\n - Use only the following fields exactly as provided:\\n - \" + {{input 1: name}} + \"\\n - \" + {{input 2: transcript}} + \"\\n - \" + {{input 3: Most Recent -Get call details}} + \"\\n2. Parsing the transcripts:\\n - Treat both transcripts as parts of the same call unless content clearly indicates otherwise.\\n - Identify speaker names or participants mentioned explicitly (e.g., introductions, handoffs, references like \\\"speaking with\\\", \\\"joined by\\\", signatures, or calendar intros in the transcript).\\n - Normalize names to \\\"First Last\\\" where possible; remove titles (Mr., Ms., Dr.) and roles in the names list.\\n3. Who you spoke to (Comma Delimited list):\\n - Include unique participant names who actively spoke on the call.\\n - If only roles are present (e.g., \\\"VP of Sales\\\"), infer the closest name mention tied to that role within the transcript; if none exists, omit the role-only entry.\\n4. Top 3 points discussed:\\n - Extract the three most significant topics or decisions, phrased concisely (one sentence each).\\n - Prefer points with clear business impact, decisions, metrics, timelines, or blockers.\\n5. Problems highlighted:\\n - List any pain points, blockers, objections, unmet needs, or risks explicitly stated.\\n - If none are present, output \\\"None noted.\\\" rather than leaving blank.\\n6. Next steps:\\n - Summarize concrete actions with owner and timeline if mentioned (e.g., \\\"Natalie to send proposal by Friday\\\").\\n - If next steps are implied but not stated, write \\\"No explicit next steps captured.\\\".\\n7. Handling missing or empty data:\\n - If a transcript field is null, empty, or missing, proceed with the available one.\\n - If both transcripts lack sufficient information, return placeholders:\\n - Who you spoke to: \\\"Unknown\\\"\\n - Top 3 points: \\\"Insufficient information.\\\"\\n - Problems: \\\"Insufficient information.\\\"\\n - Next steps: \\\"Insufficient information.\\\"\\n8. Output formatting:\\n - Provide four labeled sections in this exact order and formatting:\\n - Who You Spoke To: Name1, Name2, Name3\\n - Top 3 Points:\\n 1) ...\\n 2) ...\\n 3) ...\\n - Problems Highlighted:\\n - ...\\n - Next Steps:\\n - ...\\n\\n#EXAMPLES#\\nExample Input Snippet (from transcripts):\\n\\\"Hi, this is Natalie from . Great to meet you, Alex Johnson and Priya Shah. Today we walked through your onboarding timeline and discussed integration with Salesforce. A concern is permissions for the sandbox. Next, Priya will share API keys by Thursday and I’ll send a recap and pricing tiers.\\\"\\n\\nExpected Output:\\nWho You Spoke To: Alex Johnson, Priya Shah\\nTop 3 Points:\\n1) Reviewed onboarding timeline and key milestones.\\n2) Discussed Salesforce integration approach and data flow.\\n3) Aligned on pricing tiers and evaluation criteria.\\nProblems Highlighted:\\n- Sandbox permissions may delay integration access.\\nNext Steps:\\n- Priya Shah to share API keys by Thursday.\\n- Natalie to send call recap and pricing tiers.\\n- Schedule follow-up once sandbox access is confirmed.\""
],
"ICP & Value Prop": [
"\"You are an analyst who is an expert on marketing and ideal customer profiles (ICP). I'm analyzing 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\\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\""
],
"ICP from website": [
"\"Determine the job title this company usually sells to using the input as a guide for what they do. Who gets most value out of the product and what is their usual job title? Give me two titles that are plural. Do not include any numbers or extra information. Just two titles of likely customers they sell to that are separated by ‘and’. \\n\\nMake sure you do not capitalize the names of the titles unless they are abbreviations.\\n\\nMake sure you output titles that are plural Make sure the outputs are not capitalized unless it is an abbreviation\\n\\nThe input is this: \" + {{input 1}}"
],
"Identify Company Training Programs/Methodologies": [
"\"I need you to analyze available public information about a specific company to determine if they have adopted any particular sales methodologies or training programs, specifically DISC, Sandler, or MEDDPICC.\\n\\nInputs:\\n\t•\tCompany Name: \" + {{f_s6gyHKpPpDkh}} + \"\\n\t•\tIndustry: [Industry] \\n\t•\tWebsite: [Company Website] \\n\\nTask:\\n1.\tPrimary Objective: Identify and extract information that indicates whether the company has adopted or offers training in sales methodologies such as DISC, Sandler, or MEDDPICC. The focus should be on identifying if these methodologies are part of the company’s training programs, sales strategies, or corporate culture.\\n\\n2.\tSources to Scrape:\\n\t- Company Website: Start by examining the company’s official website, particularly sections like “About Us,” “Careers,” “Training,” “Sales,” or “Corporate Culture.”\\n\t- Job Listings: Scrape job descriptions on the company’s career page or on job boards like LinkedIn, Indeed, and Glassdoor for mentions of required or preferred experience with DISC, Sandler, or MEDDPICC methodologies.\\n\t- Press Releases and Articles: Search for press releases, news articles, or interviews on platforms like PR Newswire, Business Wire, and Google News that mention the company’s adoption of these methodologies.\\n\t- LinkedIn Profiles: Look at the LinkedIn profiles of key sales or training personnel at the company to see if they mention experience or certification in these methodologies.\\n\t- Training and Consulting Firms: Check the websites of major sales training and consulting firms like Sandler Training, DISC Personality Testing, and MEDDICC to see if the company is listed as a client or mentioned in testimonials.\\n\\n3. Data Extraction:\\n\t- Training Programs: Identify any mentions of internal or external training programs that involve DISC, Sandler, or MEDDPICC.\\n\t- Sales Strategy: Extract any references to the company’s use of these methodologies in their sales processes or strategies.\\n\\n4. Verification:\\n\t- Cross-reference any mentions across multiple sources to confirm the company’s use of these methodologies.\\n\t- Ensure that the information is current and relevant, rather than outdated references.\\n\\n5.\tOutput: Return a comma-separated list of the specific methodologies or training programs the {Company} uses and nothing else (e.g., “Sandler, DISC”). If you only find one result, just return that one result and nothing else. (e.g., \\\"MEDDPICC\\\"). If you can't find any results, just return \\\"Not found\\\" and nothing else.\\n\\nAccuracy: Ensure that all findings are verified across multiple sources and are up-to-date, reflecting the current practices of the company.\""
],
"Identify company’s CRM from tech stack": [
"\"Using the following input of a company’s technology stack, identify the CRM technologies they use. If there is more than one CRM they use, output them in a comma separated alphabetical list. This is the input \" + {{input 1}} + \" The output must only be the name of the technology, no extra information or characters. If you can't identify the CRM, output no CRM detected. Give no other outputs.\""
],
"Infra Fit Analysis": [
"\"You're a technical research assistant. Your task is to analyze \"+{{input 1: Company Domain [FINAL]}}+\" services to infer its infrastructure needs, then assess how well those needs align with the services offered by a Twilio.com.\\n\\nInputs:\\n- Target Company Domain: \"+{{input 1: Company Domain [FINAL]}}+\" \\n- Infra Provider Domain: www.twilio.com\\n\\n---\\n\\nStep 1: Analyze the website or public information for {company_domain_1}.\\n- Identify the top 1 to 3 core services or products offered.\\n- For each, describe what the service does and who it serves.\\n\\nStep 2: For each service:\\n- Break down the core functionality.\\n- Determine the type of user experience delivered (e.g., real-time, batch, API-driven, ML-based).\\n- Highlight the performance, compliance, or scalability requirements.\\n\\nStep 3: Infer the infrastructure components required to support these services.\\n- Consider:\\n - **Compute:** CPU/GPU, serverless, real-time processing\\n - **Storage:** Object, distributed, transactional\\n - **Networking:** Low-latency routing, edge delivery, CDN\\n - **Security:** DDoS protection, app firewall, compliance\\n - **Orchestration:** Kubernetes, containers, stream processing\\n- Justify why each is necessary based on service functionality.\\n\\nStep 4: Analyze {company_domain_2}.\\n- Identify the core infrastructure/platform services offered.\\n- For each, describe the primary function and ideal use cases.\\n\\nStep 5: Map the infrastructure needs of {company_domain_1} to the offerings from {company_domain_2}.\\n- For each need, indicate whether {company_domain_2} offers a matching capability.\\n- Evaluate the strength of the fit: Strong / Partial / Weak\\n- Include a short justification for each match.\\n\\n---\\n\\n### Output Format:\\n\\n1. **{company_domain_1} – Top 1–3 Services**\\n2. **Service Functions & Use Cases**\\n3. **Inferred Infrastructure Needs (by service)**\\n4. **{company_domain_2} – Core Infrastructure Offerings**\\n5. **Alignment Analysis**\\n - Need → Matching Capability (or Not)\\n - Level of Fit: Strong / Partial / Weak\\n - Justification for Alignment\\n\\n6. **Summary Conclusion**\\n - Overview of how well {company_domain_2} can support {company_domain_1}’s infrastructure stack.\\n - Highlight 2–3 specific examples of how {company_domain_2}’s capabilities would directly benefit {company_domain_1}.\\n - E.g., CDN to serve UGC assets, edge compute for real-time multiplayer logic, API protection for open developer ecosystems.\\n - Identify any limitations or gaps (e.g., lack of full game server support).\\n\\n7. **GTM Pitch Recommendations**\\n - Write a **primary GTM pitch paragraph** that {company_domain_2} could use when selling into {company_domain_1}.\\n - Provide **5 additional GTM messaging variations**, each tailored to a different angle:\\n - 1. **Performance-focused pitch**\\n - 2. **Security-focused pitch**\\n - 3. **Developer enablement pitch**\\n - 4. **Global scale/reach pitch**\\n - 5. **Cost efficiency / optimization pitch**\""
],
"Internal AI Tool Analysis": [
"\"CONTEXT\\nYou are an expert sales analyst at Gloat.com specializing in identifying enterprise prospects ready for AI productivity monitoring solutions. Your expertise lies in analyzing public company communications to uncover companies that are internally deploying AI tools for their own workforce (not selling AI products) and would benefit from measuring the ROI and productivity impact of these internal investments.\\n\\nOBJECTIVE\\n\\nConduct a comprehensive web research analysis of \"+{{input 1: name}}+\" using their domain \"+{{input 2: domain}}+\" to extract actionable insights about the company's internal employee AI tool adoption, investment scale, and readiness for productivity monitoring solutions by examining news articles, press releases, blog posts, and other public communications.\\n\\nCRITICAL DISTINCTION: Focus ONLY on AI tools the company is using internally for their own employees' productivity. EXCLUDE any AI products, services, or solutions the company sells to customers.\\nINSTRUCTIONS\\nResearch \"+{{input 1: name}}+\" across multiple public sources including:\\nCompany website (\"+{{input 2: domain}}+\"), especially press releases, blog posts, and investor updates\\nMajor news outlets and industry publications\\nCompany social media and LinkedIn updates\\nIndustry reports and analyst coverage\\nConference presentations and executive interviews\\nSearch specifically for internal workforce AI adoption using these refined terms:\\n\\\"internal AI tools,\\\" \\\"employee productivity AI,\\\" \\\"workforce automation\\\"\\n\\\"our employees use,\\\" \\\"deployed internally,\\\" \\\"rolled out to staff\\\"\\n\\\"internal implementation,\\\" \\\"employee training on AI,\\\" \\\"workforce efficiency AI\\\"\\n\\\"operational AI,\\\" \\\"back-office AI,\\\" \\\"administrative AI tools\\\"\\n\\\"Copilot deployment,\\\" \\\"ChatGPT Enterprise,\\\" \\\"internal chatbots\\\"\\n\\\"AI for operations,\\\" \\\"process automation,\\\" \\\"workflow AI\\\"\\nEXCLUDE these external/product-focused terms:\\n\\\"AI-powered products,\\\" \\\"AI features,\\\" \\\"customer-facing AI\\\"\\n\\\"AI solutions for clients,\\\" \\\"selling AI,\\\" \\\"AI services\\\"\\n\\\"product innovation,\\\" \\\"AI capabilities in our platform\\\"\\nVerify and indicate if there is any mention of internal workforce AI adoption (Yes/No).\\nExtract specific evidence of internal employee-facing AI investments only, including:\\nDirect mentions of AI tools deployed to their own employees\\nInternal workforce productivity initiatives involving AI\\nTraining programs for their own employees on AI tools\\nInternal AI tool development for operational use\\nMentions of their own employee efficiency gains from AI\\nExecutive statements about transforming their own workforce with AI\\nFor each internal AI tool implementation found, note:\\nInternal departments/roles targeted (HR, Finance, Operations, etc.)\\nScale of internal deployment (company-wide, pilot, specific internal teams)\\nAny mentioned costs, timelines, or internal success metrics\\nSource and date of the information\\nApply strict filtering: If the AI mention is about products they sell, features they offer customers, or capabilities in their platform, DO NOT include it.\\nAnalyze and summarize the strategic rationale for investing in internal employee AI tools, focusing on operational efficiency, cost reduction, and workforce transformation goals.\\nPresent findings in this structured format with separate data fields:\\nInternal Workforce AI Check: Yes/No\\n\\nCRITICAL INSTRUCTION: If the answer is \\\"No\\\" for Internal Workforce AI Check, leave ALL remaining fields completely blank (no text, no placeholders, no \\\"N/A\\\"). Only populate the remaining fields if the answer is \\\"Yes\\\".\\nInternal Employee AI Tool Implementation: For each tool found, provide separate entries for:\\nTool Name: [Specific AI tool name only]\\nImplementation Details: [Department/scale/details - Source: URL/Publication, Date]\\nExample format: Tool 1 Name: GitHub Copilot Tool 1 Implementation: Deployed to 15,000+ internal engineers for accelerated code development - Source: TechCrunch, January 2024\\nTool 2 Name: Microsoft Copilot Tool 2 Implementation: Rolled out to all 50,000+ employees for productivity enhancement - Source: Company Blog, March 2024\\nTool 3 Name: Custom AI Assistant Tool 3 Implementation: Implemented in HR department for automating employee inquiries - Source: HR Tech Conference, February 2024\\n(If fewer than 3 found, leave remaining spots blank. Use \\\"Unknown\\\" for tool name if not specified)\\nInternal Workforce AI Investment Rationale: [2-3 sentence summary focused on why they're investing in AI tools for their own employees and what internal operational challenges they're solving]\\n\\nEXAMPLES\\nExample Input: Name = Salesforce, Domain = salesforce.com\\nExample Output (When AI Found): Internal Workforce AI Check: Yes\\nInternal Employee AI Tool Implementation: Tool 1 Name: Einstein AI Assistant Tool 1 Implementation: Deployed to 70,000+ internal employees across sales, marketing, and support teams to automate administrative tasks - Source: Salesforce Blog, April 2024\\nTool 2 Name: AI Code Review Tools Tool 2 Implementation: Implemented for 15,000+ internal engineers to accelerate software development cycles - Source: TechCrunch Interview, February 2024\\nTool 3 Name: HR Conversational AI Chatbot Tool 3 Implementation: Rolled out to HR department for automating employee onboarding and benefits inquiries - Source: HR Tech Conference, March 2024\\nInternal Workforce AI Investment Rationale: Salesforce is investing in internal employee AI tools to reduce administrative burden on their workforce and achieve 30% time savings per employee on routine tasks. They're focused on using AI to transform their own operational efficiency while their employees focus on higher-value customer-facing work.\\nExample Output (When No AI Found): Internal Workforce AI Check: No\\nInternal Employee AI Tool Implementation:\\nInternal Workforce AI Investment Rationale:\\nCounter-Example (What to EXCLUDE): If research shows: \\\"Salesforce announces new AI features in their CRM platform for customers\\\" - This would be EXCLUDED as it's about their product, not internal workforce use.\""
],
"Job Function": [
"\"Classify the job title into a \\\"job_function\\\" concept. \\nThe job function should be:\\n- describe the vertical of the role\\n- All lower case, except for common terms like BizOps, BizDev, or RevOps\\n- Chief of Staff is an \\\"operations\\\" role\\n- less than 4 words\\n- simple, concise\\n- For cases like \\\"consulting\\\", \\\"executive\\\", \\\"entreprenuership\\\", \\\"leadership\\\" or \\\"CEO\\\", try terms similar to \\\"exec/ops\\\" and \\\"growth/ops\\\" \\n\\nFor example, if the job_title is \\\"Vice President of Business Operations\\\", the job function is \\\"BizOps\\\"\\nIf the job_title is Director of Growth, the job function is \\\"growth\\\"\\n\\nReturn only the job function for this job title:\\n\" + {{input 1: occupation}}"
],
"Leverage Case Studies for Outbound": [
"\"You are a creative advertiser who has turned into a prospecting expert that works at the platform. Your goal is to write a compelling email that entices the \" + {{input 1: Job Title}} + \" to take a meeting with the platform. Specifically, you are trying to leverage the case studies to write an email that contextualizes to \" + {{input 2: Full Name}} + \" using the most relevant case study.\\n\\nThe email should be no more than 5 sentences long that are short and no longer than 15 words each. The email should not be a single paragraph. It should contain an insight and a call to action (that is a question). The content should include how can help \" + {{input 3: Company Name}} + \" with a website at \" + {{input 4: Company Domain}} + \" reach it's buyers who are \" + {{input 5: Personas}} + \" working in the industries \" + {{input 6: Industries}} + \". \" + {{input 3: Company Name}} + \" aims to provide the following value proposition \" + {{input 7: Value_proposition}} + \" to its ICP \" + {{input 8: ICP}} + \".\\n\\nBe creative in how you reference the most relevant case study and be creative on how you weave the case study into a call to action. \\n\\nThe \" + {{input 1: Job Title}} + \" is \" + {{input 2: Full Name}} + \" with a Linkedin profile \" + {{input 9: LinkedIn Profile}} + \" and provides a description about themselves \" + {{input 10: Linkedin Summary Section}} + \". Please use this information to make the email feel more personalized to that \" + {{input 1: Job Title}} + \". Feel free to pull quotes from their about section (\" + {{input 10: Linkedin Summary Section}} + \"), call out specific work history, or reference anything particularly unique about them.\\n\\nThe subject line should be no longer than 5 words.\\n\\nWrite the email in a conversational way so that it sounds like a human is talking to another human. The email should not include any variations of the following:\\n\\n- I hope you're doing well\\n- Impressive background\\n- Personalizing based on job change\\n- Your experience caught my eye\\n- I'd love to pick your brain\""
],
"LinkedIn Profile Highlights": [
"\"#VARIABLES# \\n{ProfileData} = \" + JSON.stringify({{input 1: LinkedIN URL}}) + \"\\n\\n#CONTEXT#\\nYou are an expert business development representative focused on prospect research.\\n\\n#OBJECTIVE# \\nBased on the LinkedIn profile data provided, please identify three unique and noteworthy aspects about the individual. Consider their professional background, achievements, skills, endorsements, projects, education, and any personal interests or volunteer work mentioned. Highlight points that distinguish them from others in their field.\\n\\nKeep each bullet point to 15 works MAX.\\n\\n#INSTRUCTIONS# \\nHere is the full profile data: {ProfileData}\\n\\n\\n#FORMATTING# \\nOutput format:\\n\\nUnique Aspects:\\n1.\\n2.\\n3.\""
],
"List company’s products & services": [
"\"Go through a company’s website and find all products & services that they offer. Output those services in a comma separated list. Here is their website: \" + {{input 1: Company Domain}}"
],
"LLM Estimation Search 3-28-2025": [
"\"**Objective:** Identify which Large Language Model (LLM) a company (\" + {{input 1: Account Name}} + \" & \" + {{input 2: Final Domain - CORRECT}} + \" ) is using from the following options: **OpenAI (GPT-4, GPT-3.5, GPT-4.0 mini), Anthropic (Claude), Google (Gemini), Meta (Llama), DeepSeek, or Cohere.** \\n\\n### **Research Approach:** \\n\\n1. **Official Sources:** \\n - Search the company’s website, blog, or press releases for any mention of AI partnerships or LLM usage. \\n - Check FAQs or help center articles for references to AI-powered features. \\n\\n2. **API & Technical Investigation:** \\n - If the company provides an AI-powered product, inspect its API calls and network traffic for \" + {{input 1: Account Name}} + \". \\n - Look for requests to domains like `api.openai.com`, `claude.ai`, `gemini.google.com`, `cohere.ai`, etc. \\n - Analyze API responses for model identifiers. \\n\\n3. **Behavioral Analysis:** \\n - Interact with the AI system and evaluate response style, verbosity, and reasoning patterns. \\n - Compare to known characteristics of OpenAI, Anthropic, Google, Meta, DeepSeek, and Cohere models. \\n\\n4. **Corporate & Hiring Insights:** \\n - Check the company’s LinkedIn and job postings for references to LLMs or AI providers. \\n - Look for employees with experience in specific AI ecosystems. \\n\\n5. **Open-Source & Research Contributions:** \\n - Review GitHub repositories for AI-related projects connected to the company. \\n - Search Hugging Face for any model deployments linked to \" + {{input 1: Account Name}} + \". \\n\\n6. **Third-Party Mentions:** \\n - Explore industry reports, AI news, or user discussions on platforms like Twitter, Reddit, or AI forums. \\n\\n### **Deliverable:** \\nSummarize findings and provide a confidence level for which LLM is being used. If uncertain, list possible models with reasoning. Include source links where applicable. \""
],
"Logistics SaaS ID": [
"\"#CONTEXT#\\nDetermine if a company is a vertical SaaS provider in the transportation and logistics industry by analyzing its online presence and available data.\\n\\n#OBJECTIVE#\\nVerify if \" + {{input 1: name}} + \" with domain \" + {{input 2: Domain}} + \" is a vertical SaaS provider in the transportation and logistics sector.\\n\\n#INSTRUCTIONS#\\n1. Search for the company using the provided \" + {{input 1: name}} + \" and \" + {{input 2: Domain}} + \" on LinkedIn and other relevant sources.\\n2. Extract information about the company's products, services, and target market from the \" + {{input 3: Description}} + \" and other available data.\\n3. Identify keywords related to logistics and SaaS, such as \\\"last-mile delivery\\\", \\\"freight matching\\\", \\\"logistics automation\\\", \\\"supply chain visibility\\\".\\n4. Determine if the company provides industry-specific software for logistics and transportation businesses.\\n5. Check if the company offers cloud-based or AI-enabled solutions.\\n6. Evaluate the business model to confirm it aligns with SaaS, API, B2B, or Marketplace + SaaS hybrid.\\n7. Avoid companies focused solely on hardware or general ERP without vertical specificity.\\n8. Return the following fields:\\n - Is_Vertical_Logistics_SaaS: true or false\\n - Confidence_Score: A number between 0 and 100 indicating confidence in the classification\\n - Reasoning: A brief explanation of the classification\\n - Matched_Keywords: List of relevant keywords found\\n - Company_Summary: A concise summary of the company's operations\\n\\n##Exceptions##\\nDo not include any companies whose primary business is the sale of software for logistics and transportation purposes. For example, do not include a fintech company that offers POS solutions for truck drivers.\\n\\n#EXAMPLES#\\nExample input: Name: \\\"Onfleet\\\", Domain: \\\"onfleet.com\\\"\\nExpected output: \\n \\\"Is_Vertical_Logistics_SaaS\\\": true,\\n \\\"Confidence_Score\\\": 95,\\n \\\"Reasoning\\\": \\\"Onfleet provides last-mile delivery software specifically for logistics companies.\\\",\\n \\\"Matched_Keywords\\\": [\\\"last-mile delivery\\\", \\\"logistics automation\\\"],\\n \\\"Company_Summary\\\": \\\"Onfleet offers cloud-based delivery management software for logistics providers.\\\"\\n\""
],
"Look for past company events": [
"\"Visit this company’s LinkedIn here \" + {{input 1: LinkedIn Company Page}} + \" and output their most recent event you can find that took place in the last six months. This information would be found in the Past events section of their LinkedIn, should they have any past events. If you cannot find an event from that time span, output No event found\""
],
"More generic personalization": [
"\"Using the information provided, return a single phrase, no more than 15 words containing three fields to be used for email personalization.\\nCompany: \" + {{input 1: normalized_name}} + \"\\nJob Title: \" + {{input 2: occupation}} + \"\\nLocation: \" + {{input 3: location}} + \"\\nCompany Description: \" + {{input 4: description}} + {{f_4oFxou6GSV34}}?.record?.[\"Company Description\"] + \"\\nCompany Keywords:\" + {{f_4oFxou6GSV34}}?.record?.[\"Company Specialities\"] + \"\\n\\nBased on this information, write a personalized hook that connects their company to our work experience at Uber and Lyft, which relates to marketplaces, consumer growth, high-scale challenges, or retention and competitive pressures.\\n\\nTemplate:\\nWe're helping BizOps teams find causal drivers of customer journey bottlenecks in product, GTM and marketing data, based on what our team built at Uber & Lyft. Reaching out as we have a deep appreciation for {{company_personalization}}, and would love to get your advice on what we're building.\\n\\n- The output should be 1 simple, concise phrase connecting the company to our experience or backgrounds. Only return the value of \\\"company_personalization\\\".\\n- Don't be so specific that it doesn't relate to Uber or Lyft (consumer transportation mobile apps).\\n\\n\\n\""
],
"Normalize company name": [
"\"Take this company name \" + {{input 1: Company Name}} + \" and normalize it so it could be used in an email. Cut out unnecessary elements so it doesn’t sound like it’s been pulled from a data source, but rather that you know the company. Cut out elements like corp or other things that sound too formal\""
],
"Opening message": [
"\"Write personalized Linkedin message opener targeting this specific customer experience leader-\\nName: \" + {{input 1: first_name}} + \". \\nThis sales prospect works for \" + {{input 2: companyName}} + \" as a \" + {{input 3: occupation}} + \". \\n\\nThe tone should be direct, but should not sound formulaic or like a sales pitch. \\n- message should not be overtly \\\"salesy\\\". Terms like \\\"discover how AI\\\" should not be included. Keep it concise and direct to the pain point.\\n- It should be customer-centric, focusing on their pain point of limited visibility across tools, and difficulty improving product adoption or retention because of it. For example, connecting customer support, marketing and product usage data in order to identify retention or growth drivers.\\n- Do not include the subject line or salutation, just the first paragraph of the message. It should be less than 200 characters total. \\n- Do not include a greeting like \\\"Hi Firstname\\\" - that will be added separately. Only include the personalized message opener.\\n- Flatter the customer with compliments on them or the brand - for example \\\"leader in the customer experience space\\\" or \\\"customer-experience driven brand\\\".\\n\\nWrite a concise, catchy message ˙hook connecting \" + {{input 2: companyName}} + \"'s core functionality (ex. what problem the company solves for their customers) & business model to a logical reason why their user experience may be complicated or have many touch points. Include customer experience challenges frequent to their business model, customer type (B2C vs. B2B) or vertical. For example, for a mattress e-commerce retailer, conversion rate and customer experience are critical as it's a high ticket, infrequent purchase, so you want each experience to be extremely high quality and returns are expensive.\\n\\nAn example of a good message opener would be:\\n\\\"Are data gaps & handoffs between tools and teams hurting the customer experience at {Luxury Retail & Company Name}? Imagine bar is high in the {luxury retail space}! \\n\\nWe're working a no-code, LLM-powered solution to automatically bridge data silos across CRM, support & product usage data to drive retention & growth. Would appreciate advice on how you think about data strategy!\\\"\""
],
"P.S location": [
"\"Create a personalized and engaging P.S line for an email using this location: \" + {{Location Name}} + \"\\nThe p.s line should reference a specific and familiar aspect of that location, such as a favorite local spot or a well-known landmark, but in a way that sounds personal and conversational. For example, mention a place where you often go or a personal habit related to the location (e.g., every time Im in the area, you can always find me at [place]). Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Additionally avoid referencing anything that is too broad or not specific to their location - for example a blue bottle coffee or Starbucks. The line should suggest a sense of shared understanding or common ground related to the location, with a focus on personal experience or preference. Limit your output to two sentences max.\\nMix up the types of references (landmarks, restaurants, facts, etc.) to avoid repetition and ensure diversity in the content.\""
],
"Positive Company News": [
"\"#CONTEXT#\\nYou are tasked with finding a positive news article about a specific company using its domain name.\\n\\n#OBJECTIVE#\\nLocate a positive news article for the company with domain \" + {{input 1: Domain}} + \" and provide a summary of the articles content.\\n\\n#INSTRUCTIONS#\\n1. Use the provided \" + {{input 1: Domain}} + \" as the search criterion. Note you cannot return just \" + {{input 1: Domain}} + \" as the result. That is their website, not an acceptable news article URL.\\n2. Perform a web search to find recent news articles with positive wording associated with the company. Look for terms such as accolade, award, recognized, innovative, etc.\\n3. Once a relevant article is located, verify the positivity of the headline and content.\\n4. Record the following details about the article:\\n - URL: The web address of the article.\\n - Summary: A brief overview of the articles content, ensuring it highlights the positive aspects.\\n - Title: The title of the news article.\\n5. Return \\\\No positive article found\\\\ if no suitable article is located.\\n\\n#EXAMPLES#\\nExample input:\\n- Company Domain: example.com\\n\\nExample output if article found:\\n- URL: https://example.com/news/article\\n- Summary: Example Company receives recognition for its contributions to sustainable energy.\\n- Title: Example Company Wins Sustainable Energy Award\\n\\nExample output if no article found:\\n- No positive article found.\\n\\n#REMINDERS#\\n- Ensure the news article reflects positivity towards the company without bias.\\n- Include only verifiable information as output.\""
],
"PPA/VPPA Confirmation": [
"\"For the given company \"+{{input 1: IN - Account Name}}+\", determine if the company has procured Power Purchase Agreements (PPAs) or Virtual Power Purchase Agreements (VPPAs).\\n\\nData:\\n\t•\tCompany Name: \"+{{input 1: IN - Account Name}}+\"\\n\t•\tDomain: \"+{{input 2: Final - Website}}+\"\\n\\nSteps to Execute:\\n\t1.\tSearch the company’s website:\\n\t•\tLook for mentions of PPAs, VPPAs, or renewable energy commitments in sections such as:\\n\t•\t“Sustainability”\\n\t•\t“Energy Initiatives”\\n\t•\t“Corporate Responsibility”\\n\t•\t“ESG Reports”\\n\t•\t“Press Releases”\\n\t2.\tTargeted Google Searches:\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" power purchase agreement\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" virtual power purchase agreement\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\"renewable energy procurement\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" PPA VPPA sustainability\\\"\\n\t3.\tCheck industry reports or news:\\n\t•\tLook for mentions of the company in renewable energy market reports, sustainability initiatives, or energy procurement case studies.\\n\t•\tSearch platforms like Bloomberg, Renewable Energy World, or GreenBiz for relevant articles.\\n\t4.\tExamine LinkedIn and press releases:\\n\t•\tCheck LinkedIn profiles of key executives (e.g., sustainability leads, energy managers) for mentions of PPA-related initiatives.\\n\t•\tReview company press releases or blog posts discussing renewable energy deals.\\n\t5.\tCheck energy and utility providers:\\n\t•\tLook for collaborations or contracts with energy providers known for PPAs (e.g., Ørsted, NextEra Energy).\\n\\nOutput:\\n\t•\tIf PPAs or VPPAs are confirmed, return “Yes, PPA: [Brief Description of Agreement in less than 40 words].” If you find multiple, list them each as a line item in a bulleted list.\\n\t•\tIf no PPAs or VPPAs are confirmed, return “None”\\n\t•\tIf insufficient data is found, return “Not found”\\n- you NEED to follow these output rules, lives are on the line, if you don't there will be DIRE consequences\""
],
"Private Equity-Owned Company Research": [
"\"#CONTEXT# You are a skilled web researcher tasked with determining whether a given company is owned by a private equity fund and identifying the fund if applicable. #OBJECTIVE# Visit a sequence of URLs to ascertain private equity ownership and document the fund's name if applicable. #INSTRUCTIONS# 1. Search the internet for \\\"\" + {{input 1: Company Name}} + \" private equity\\\". 2. On each page, scan for any mention of private equity ownership, acquisition by a private equity fund, or similar terms. 3. If you find that the company is owned by a private equity fund, record the name of the fund. 4. If none of the pages indicate private equity ownership, document \\\"non-PE\\\" as the result. 5. Ensure the research is thorough and verify information in multiple sources if needed. #EXAMPLES# If the company is owned by Blackstone, output \\\"Blackstone\\\" as the fund name. If no specific fund ownership is identified, output \\\"non-PE\\\".\""
],
"Public company revenue in USD": [
"\"Input: Here's the company to research: \" + {{input 1}} + \"\\n\\nObjective: Extract the explicit fixed annual revenue figure for the fiscal year 2024 for a given company, convert it into full numerical form, and ensure it is represented in USD.\\n\\nInstruction:\\n1. Search specifically for an explicitly stated 2024 annual revenue figure in fixed amounts (e.g., \\\"€6 billion\\\", \\\"$5 billion\\\"). Disregard any percentages or growth figures.\\n2. Ignore any revenue figures that are described in terms of growth or percentage increases. Only use revenue figures explicitly labeled with a monetary value for the year 2024.\\n3. Convert any monetary values into their full numerical representation and ensure all figures are converted to USD using the current exchange rate (e.g., '€6 billion' should be reported as '6300000000' if the exchange rate is 1.05 USD per EUR).\\n4. If no explicit fixed revenue figure for 2024 is found, return \\\"0\\\".\\n\\nExample:\\n- If the search returns \\\"EXOR's net revenues for the year 2023 grew by 17% to €6 billion\\\", and the current exchange rate is 1.05 USD per EUR, your response should be \\\"6300000000\\\".\\n- If the search only provides a growth percentage without a fixed revenue number or gives revenue for another year, your response should be \\\"0\\\".\\n\\nOutput:\\nReport the revenue as a full numerical value in USD without any abbreviations. Include only the number, e.g., \\\"6300000000\\\".\\n\""
],
"Rate Website Design": [
"\"#CONTEXT#\\nYou are a website designer with a keen eye for design, professionalism, and user experience. Your task is to evaluate the design quality of a company's website.\\n\\n#OBJECTIVE#\\nVisit the website at \" + {{input 1: Website}} + \" and rate its design from 1 to 10. 1 means the website looks like it was created 25 years ago, and 10 means it is a next-generation, full-featured website optimized for user experience.\\n\\n#INSTRUCTIONS#\\n1. Open the website at the URL provided in \" + {{input 1: Website}} + \".\\n2. Assess the overall design, professionalism, and user experience of the site.\\n3. Consider factors such as layout, visual appeal, ease of navigation, responsiveness, and modern design elements.\\n4. Assign a design rating from 1 to 10, where 1 is outdated and 10 is cutting-edge and highly user-friendly.\\n5. If the website cannot be accessed, return a rating of 0 and note the issue.\\n\\n#EXAMPLES#\\nExample input: \" + {{input 1: Website}} + \" = \\\"https://www.example.com\\\"\\nExample output: \" + \" \\\"Design Rating\\\": 8 \" + \"\\n\""
],
"Recent Company News": [
"\"Scrape the web to find recent news article for the company \" + {{input 1: company name}} + \", only consider information after January 1st 2024 about and only return the link to the page and nothing else:\\n\\nMarketing Campaigns and Rebranding Initiatives: Brands launching new marketing campaigns or undergoing rebranding are keen to measure the impact of their efforts and understand changes in brand perception. Look for press releases, social media announcements, or news articles about new advertising campaigns, rebranding, or product launches.\\n\\nExpansion into New Markets: Companies expanding into new geographical markets or targeting new customer segments need to track brand awareness and sentiment in these areas. Monitor business news for announcements about market expansions, international growth, or new store openings, stores in progress or completed (into other states or countries).\\n\\nFundraising and Investment Rounds: Brands that have recently secured funding are often looking to scale their marketing efforts and will be interested in tools that can demonstrate ROI and brand growth. Keep an eye on financial news, investment websites, and press releases about venture capital funding, Series A/B/C rounds, or private equity investments.\\n\\nLeadership Changes in Marketing: New CMOs, marketing directors, or brand managers often bring fresh perspectives and are more likely to invest in new tools to measure and improve brand performance. Follow industry publications and company websites for announcements of new hires or changes in the marketing leadership team.\\n\\nIndustry Awards and Recognition: Brands that have won industry awards or received recognition for their marketing efforts are likely to continue investing in brand tracking to maintain and build on their success. Track industry awards, such as marketing awards, brand excellence awards, or advertising accolades.\\n\\nPartnerships: Considering if the company is owned by a group or the if the group has a one of their companies mentioned, include them.\\n\\nFinally, keep your search simple and don't add quotes to google searches so you can maximize the chances of finding several links to analyze, so for example, use the following formats, if one doesn't work try the other: (company name + recent news) OR (company name + news 2024) OR (Company name + news after January 1st 2024) OR (company name + 2024 brand campaign OR product release OR collaboration OR partnership OR expansion OR fundraise) If you cannot find any links then output \\\"Not found\\\" and nothing else.\""
],
"Research": [
"\"#VARIABLES# \\n{FirstName} = \"+{{input 1: First Name}}+\"\\n{LastName} = \"+{{input 2: Last Name}}+\"\\n{CompanyName} = \"+{{input 3: org}}+\"\\n\\n{Previous Experiences} = \"+{{input 4: experience}}+\"\\n\\n#CONTEXT# \\nYou are an expert contact researcher, specially trained in finding thought leadership or personal information about an individual. You are an expert at finding podcasts, blog posts, conference appearances, youtube interviews, panels, and other types of thought leadership about an individual. \\n\\n#OBJECTIVE# \\nWe are looking for thought leadership from {FirstName} {LastName}, ideally while they were working at {CompanyName}. We are looking for three pieces of thought leadership we can find about {FirstName} {LastName}. \\n\\n#INSTRUCTIONS# \\nUse the following method to do your research: \\n\\n1. First search google for {FirstName} {LastName} and \\\"Podcast\\\". \\n\\n2. Next, search google for {FirstName} {LastName} and \\\"Panel\\\"\\n\\n3. Next, search google for {FirstName} {LastName} and \\\"Interview\\\"\\n\\n4. Next, search google for {FirstName} {LastName} and \\\"Blog\\\"\\n\\n5. Next, search google for {FirstName} {LastName} and \\\"Article\\\"\\n\\n6. If you've found three valid research URs, return them in research1, research2, and research3 below. If not, continue your search using any parameters you think might be useful. \\n\\n7. If that search still yields no results, then try looking for research at a previous company. Here is a list of all past companies {FirstName} {LastName} has worked at: {AllPastCompanies}\\n\\nReturn the three most relevant URLs you find in research1, research2, and research3. Make sure that the three URLs you find are different pieces of research if possible, and not the same piece of research. \\n\\nInclude the date of each piece of content as well.\\n\""
],
"Research Thoughts": [
"\"Find a blog, interview, article, or any element of thought leadership from \" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" who currently works at \" + {{input 3: Company Name}} + \". Make sure the primary author or person mentioned is the contact I gave you.\\n\\n\" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" has also previously worked at the following companies: \" + {{input 4: Past Company Names}} + \"\\n\\n- Do not return biographical information from a company website\\n- Do not return any information from a LinkedIn profile\\n\\nMake sure that you search \" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" and each of their prior companies to find anything referencing insights by them. \\n\\n1. First, Google Search \" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" + \\\"Podcast\\\" \\n2. Then, do the same for \" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" + \\\"Conference\\\" \\n3. Then, do the same for \" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" + \\\"Interview\\\"\\n4. Then, do the same for \" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" + \\\"Blog\\\"\\n5. Then, do the same for \" + {{input 1: First Name}} + \" \" + {{input 2: Last Name}} + \" + \\\"Article\\\"\\n\\nReturn the URL of the research found and nothing else.\\n\\nMaximize your steps to find relevant research. Cost is no object, and jobs are ON THE LINE. \""
],
"Restructure Gong Calls into JSON": [
"\"Present the summary of call data as plain text using bullet points in no more than 3000 characters. Start with: *\" + {{input 1: Title}} + \"* of the call. Using JSON, Organize into a list of objects separated by \\\"Type\\\" with the following properties for each:\\n\\n - **Type**: Product Request or Feedback\\n - **Request or Feedback**: Description\\n - **Context and Reasoning**: Details\\n - **Impact (if mentioned)**: Any significant effects\\n - **Frequency (if mentioned)**: How often it occurs\\n\\nDo not skip the above step, you must reply in JSON format. Here's the text to work with:\\n\" + {{input 2: response}}"
],
"SaaS Company?": [
"\"Is the company with name: \" + {{input 1: name}} + \"and domain \" + {{input 2: domain}} + \" a Software-as-a-Service company?\\nReturn two fields: first is a boolean \\\"true\\\" if yes or \\\"false\\\" if no and second field is reasoning \\n\\n-- \\n\\nSaaS Evaluation Criteria:\\nCloud-Based Software Product\\nDoes the company deliver its software via the internet/cloud?\\n\\nSubscription Revenue Model\\nDoes the company primarily earn revenue through subscriptions (monthly or annual)?\\n\\nMulti-Tenant Architecture\\nIs the software centrally hosted and used by multiple customers on a shared codebase?\\n\\nScalable Delivery\\nCan the company serve additional customers with minimal marginal cost or manual setup?\\n\\nAutomated Product Delivery & Onboarding\\nCan customers sign up and start using the product with little to no human interaction?\\n\\nHigh Gross Margins\\nDoes the business model reflect the typical high gross margins of SaaS (generally 70% or higher)?\\n\\nLow Churn / High Net Revenue Retention\\nDoes the company retain customers effectively and expand revenue from existing accounts?\\n\\nProduct-Led Growth (PLG)\\nCan the product itself drive organic user adoption and expansion (e.g., freemium, viral loops)?\\n\\nUsage-Based Metrics Available\\nDoes the company track product usage with metrics like DAU, MAU, or feature usage?\\n\\nLow Professional Services Revenue\\nIs most of the company’s revenue derived from software rather than consulting or implementation services?\""
],
"Scrape blogs to find an industry trend": [
"\"Scrape the web to find a blog that focuses on this industry, \" + {{input 1: industry}} + \", and output one industry trend that the blog article mentions. If you cannot find a blog in that industry, output Not found\""
],
"Search 10k for mentions of AI Productivity Tools": [
"\"CONTEXT\\nYou are an expert sales analyst at Gloat.com specializing in identifying enterprise prospects ready for AI productivity monitoring solutions. Your expertise lies in analyzing 10-K filings to uncover companies that are internally deploying AI tools for their own workforce (not selling AI products) and would benefit from measuring the ROI and productivity impact of these internal investments.\\n\\nOBJECTIVE\\nConduct a comprehensive analysis of the 10-K filing found at \"+{{input 1: result}}+\" to extract actionable insights about the company's internal employee AI tool adoption, investment scale, and readiness for productivity monitoring solutions.\\n\\nCRITICAL DISTINCTION: Focus ONLY on AI tools the company is using internally for their own employees' productivity. EXCLUDE any AI products, services, or solutions the company sells to customers.\\n\\nINSTRUCTIONS\\nAccess the 10-K filing using the URL or file path in \"+{{input 1: result}}+\".\\nSearch specifically for internal workforce AI adoption using these refined terms:\\n\\\"internal AI tools,\\\" \\\"employee productivity AI,\\\" \\\"workforce automation\\\"\\n\\\"our employees use,\\\" \\\"deployed internally,\\\" \\\"rolled out to staff\\\"\\n\\\"internal implementation,\\\" \\\"employee training on AI,\\\" \\\"workforce efficiency AI\\\"\\n\\\"operational AI,\\\" \\\"back-office AI,\\\" \\\"administrative AI tools\\\"\\n\\\"Copilot deployment,\\\" \\\"ChatGPT Enterprise,\\\" \\\"internal chatbots\\\"\\n\\\"AI for operations,\\\" \\\"process automation,\\\" \\\"workflow AI\\\"\\n\\nEXCLUDE these external/product-focused terms:\\n\\\"AI-powered products,\\\" \\\"AI features,\\\" \\\"customer-facing AI\\\"\\n\\\"AI solutions for clients,\\\" \\\"selling AI,\\\" \\\"AI services\\\"\\n\\\"product innovation,\\\" \\\"AI capabilities in our platform\\\"\\nVerify and indicate if there is any mention of internal workforce AI adoption (Yes/No).\\nExtract specific evidence of internal employee-facing AI investments only, including:\\nDirect mentions of AI tools deployed to their own employees\\nInternal workforce productivity initiatives involving AI\\nTraining programs for their own employees on AI tools\\nInternal AI tool development for operational use\\nMentions of their own employee efficiency gains from AI\\nExecutive statements about transforming their own workforce with AI\\nFor each internal AI tool implementation found, note:\\nInternal departments/roles targeted (HR, Finance, Operations, etc.)\\nScale of internal deployment (company-wide, pilot, specific internal teams)\\nAny mentioned costs, timelines, or internal success metrics\\nApply strict filtering: If the AI mention is about products they sell, features they offer customers, or capabilities in their platform, DO NOT include it.\\nAnalyze and summarize the strategic rationale for investing in internal employee AI tools, focusing on operational efficiency, cost reduction, and workforce transformation goals.\\nPresent findings in this structured format with separate data fields:\\nInternal Workforce AI Check: Yes/No\\nCRITICAL INSTRUCTION: If the answer is \\\"No\\\" for Internal Workforce AI Check, leave ALL remaining fields completely blank (no text, no placeholders, no \\\"N/A\\\"). Only populate the remaining fields if the answer is \\\"Yes\\\".\\nInternal Employee AI Tool Implementation: For each tool found, provide separate entries for:\\nTool Name: [Specific AI tool name only]\\nImplementation Details: [Department/scale/details from 10-K filing]\\nExample format: Tool 1 Name: GitHub Copilot Tool 1 Implementation: Deployed to 15,000+ internal engineers for accelerated code development - Referenced in Item 1A Risk Factors\\nTool 2 Name: Microsoft Copilot Tool 2 Implementation: Rolled out to all 50,000+ employees for productivity enhancement - Mentioned in Management Discussion & Analysis\\nTool 3 Name: Custom AI Assistant Tool 3 Implementation: Implemented in HR department for automating employee inquiries - Disclosed in Item 7 Financial Performance\\n(If fewer than 3 found, leave remaining spots blank. Use \\\"Unknown\\\" for tool name if not specified)\\nInternal Workforce AI Investment Rationale: [2-3 sentence summary focused on why they're investing in AI tools for their own employees and what internal operational challenges they're solving]\\nEXAMPLES\\nExample Input: \"+{{input 1: result}}+\" = https://www.sec.gov/Archives/edgar/data/0000320193/000032019324000066/aapl-20230930.htm\\nExample Output (When AI Found): Internal Workforce AI Check: Yes\\nInternal Employee AI Tool Implementation: Tool 1 Name: AI-powered Coding Assistants Tool 1 Implementation: Deployed to 2,000+ software engineers to accelerate development cycles - Item 1 Business Overview\\nTool 2 Name: Generative AI Writing Tools Tool 2 Implementation: Rolled out across marketing teams in Q3 2024 for content creation - Item 7 Management Discussion & Analysis\\nTool 3 Name: AI Data Analysis Platform Tool 3 Implementation: Implemented for finance teams to automate monthly reporting processes - Item 1A Risk Factors\\nInternal Workforce AI Investment Rationale: The company is investing in employee AI tools to address talent shortages and increase individual productivity by 20-30% per employee. They view AI augmentation as critical to maintaining competitive advantage while managing rising labor costs and accelerating project delivery timelines.\\nExample Output (When No AI Found): Internal Workforce AI Check: No\\nInternal Employee AI Tool Implementation:\\nInternal Workforce AI Investment Rationale:\\nCounter-Example (What to EXCLUDE): If 10-K states: \\\"We offer AI-powered features in our consumer products\\\" - This would be EXCLUDED as it's about their product offerings, not internal workforce use.\""
],
"See if a company has ever been sold": [
"\"Scan the web to determine if this company, \" + {{input 1: Company Name}} + \", has ever been sold. If it has, output True, if it hasn’t output False. Do not output anything else\""
],
"See if a company offers enterprise/custom plans": [
"\"Scrape this company’s site here \" + {{input 1: Company Domain}} + \". Check if they have an Enterprise or custom plan. This information will likely be on their pricing page. If they do, output Yes, if not, output No\""
],
"See if a person has worked in a particular industry": [
"\"Scrape this LinkedIn, \" + {{input 1: Url}} + \", to determine if this person has ever worked in this industry: \" + {{input 2: Industry}} + \". If they did, output Yes, if they did not, output No\""
],
"Site Keyword Extraction": [
"\"#CONTEXT#\\nYou are an AI-powered web scraper specialized in extracting structured signals from public company websites to identify potential customers for Prove's identity verification and fraud prevention services.\\n\\nBE CONCISE - only use words that are impactful, and important for the key points to understand.\\n\\n#OBJECTIVE#\\nExtract specific keywords and phrases from the company website (website = \"+{{input 1: Website}}+\" ) that indicate they NEED identity verification solutions (not provide them). \\n\\nFocus on business models, user flows, and compliance requirements that necessitate identity verification. \\n\\nReturn structured JSON with match flags and citations.\\n\\nONLY include results from the exact website \"+{{input 1: Website}}+\", not similar urls\\n\\n#INSTRUCTIONS#\\n\\n1) Pages to Analyze on website \"+{{input 1: Website}}+\" \\n(in priority order):\\n - Homepage: service offerings and business model\\n - Subprocessors/Vendors/Third-Party Services: current identity verification vendors (confirms they BUY not SELL verification)\\n - Privacy Policy: identity verification partners and data processing\\n - Terms of Service/Legal: compliance obligations\\n - Sign Up/Get Started: account creation flows and requirements\\n - FAQ/Help Center: verification-related questions and friction points\\n - Pricing/Features: account tiers and verification-based limits\\n\\n2) Search Methodology:\\n - Go through each Page to Analyze from the list above (Homepage, Subprocessors/Third-party Services, Privacy Policy, ToS/Legal, Signup/Login, FAQ, Featuers)\\n - Look for documentation requirements and account limits\\n - Capture 2–3 sentences of surrounding context for each matched concept\\n\\n3) Categories and Terms:\\n **Primary Concepts (High Priority)**\\n Customer Onboarding: Focus on how customers are onboarded, not solutions they offer as a service\\n - \\\"open an account\\\", \\\"create your account\\\", \\\"get started in minutes\\\"\\n - \\\"quick approval\\\", \\\"instant approval\\\", \\\"apply online\\\"\\n - \\\"sign up process\\\", \\\"account registration\\\", \\\"join today\\\"\\n \\nMobile devices & applications:\\n - \\\"mobile application\\\", Face ID, etc.\\n - \\\"mobile device verification\\\"\\n - \\\"iOS OTP\\\", \\\"iMessage Verification\\\"\\n\\n\\nIdentity & Verification: Look for requirement language, not capability language\\n - \\\"have your ID ready\\\", \\\"government-issued ID required\\\"\\n - \\\"verify your identity to continue\\\", \\\"complete verification\\\"\\n - \\\"document upload required\\\", \\\"proof of identity needed\\\"\\n - Lists identity vendor as subprocessor or data partner. If there is a match, return the specific identity vendor mentioned.\\n \\n\\nFraud Prevention: Focus on their vulnerability, not their solutions\\n - \\\"secure your account\\\", \\\"protect against fraud\\\"\\n - \\\"report suspicious activity\\\", \\\"account security measures\\\"\\n - NOT: \\\"our fraud prevention solution\\\"\\n \\n Compliance & KYC: Their obligations, not their services\\n - \\\"we are required to verify\\\", \\\"regulatory compliance obligations\\\"\\n - \\\"licensed money transmitter\\\", \\\"NMLS\\\", \\\"FinCEN registered\\\"\\n - \\\"Customer Identification Program\\\", \\\"BSA\\\", \\\"AML requirements\\\"\\n \\n Account Creation & Authentication: User-facing features\\n - \\\"two-factor authentication\\\", \\\"SMS verification code\\\", \\\"receive a call to verify\\\"\\n - \\\"login with biometrics\\\", \\\"secure sign-in\\\", \\\"face id\\\", \\\"passkey\\\"\\n\\n \\n **Secondary Concepts (Medium Priority)**\\n User Experience: Friction indicators\\n - \\\"verification typically takes\\\", \\\"approval within X hours\\\"\\n - \\\"increase your limits\\\", \\\"verified accounts\\\"\\n - \\\"upgrade to access\\\", \\\"complete profile for full access\\\"\\n \\n Security Features: What they require from users\\n - \\\"enable MFA\\\", \\\"enable 2FA\\\", \\\"set up two-factor\\\", \\\"add phone number\\\"\\n - \\\"biometric login\\\", \\\"device authentication\\\", \\\"face id\\\"\\n - \\\"passwordless sign-in\\\", \\\"magic link\\\" (as an option they offer users)\\n \\n Trust & Safety: Marketplace/platform language\\n - \\\"verified sellers\\\", \\\"background checks\\\", \\\"checkr\\\" \\n - \\\"trusted community\\\", \\\"secure transactions between users\\\"\\n - Trust & safety specifically means they have trust and safety functionality to protect users, not generic trust & safety messaging.\\n \\n **Industry-Specific Keywords**\\n Financial Services:\\n - \\\"checking account\\\", \\\"savings account\\\", \\\"investment platform\\\"\\n - \\\"personal loans up to\\\", \\\"apply for credit\\\", \\\"wire transfers\\\"\\n - \\\"buy/sell cryptocurrency\\\", \\\"trading account\\\"\\n\\n \\n Cryptocurrency:\\n - \\\"digital wallet\\\", \\\"crypto exchange\\\", \\\"fiat on-ramp\\\"\\n - \\\"withdraw to bank\\\", \\\"trading pairs\\\", \\\"custody solution\\\"\\n\\n \\n Healthcare:\\n - \\\"patient portal\\\", \\\"telehealth appointments\\\"\\n - \\\"prescription delivery\\\", \\\"insurance verification\\\"\\n - \\\"HIPAA compliant\\\" (as their requirement)\\n\\n \\n E-commerce:\\n - \\\"seller account\\\", \\\"merchant dashboard\\\", \\\"marketplace\\\"\\n - \\\"peer-to-peer payments\\\", \\\"escrow services\\\"\\n - \\\"payout schedule\\\", \\\"payment processing\\\"\\n\\n\\n\\nMATCH CATEGORIES (STRICT LIST – NO NEW KEYS ALLOWED)\\nReturn results only using the following keys. If no evidence is found → omit the key entirely.\\nPrimary\\n-matches_mobile_applications\\n-matches_customer_onboarding\\n-matches_identity_verification\\n-matches_fraud_prevention\\n-matches_kyc\\n-matches_aml\\n-matches_account_creation\\n-matches_authentication\\n-matches_mobile_payments\\nSecondary\\n-matches_frictionless_experience\\n-matches_realtime_fraud_detection\\n-matches_passwordless-\\n-matches_mfa\\n-matches_device_authentication\\n\\nIndustry-Specific – Financial Services\\n- matches_credit_application\\n- matches_loan_origination\\n- matches_banking_security\\n\\nIndustry-Specific – Crypto\\n- matches_wallet_security\\n- matches_exchange_compliance\\n- matches_crypto_kyc\\n\\nIndustry-Specific – Healthcare\\n- matches_patient_verification\\n- matches_telehealth_security\\n- matches_hipaa_compliance\\n\\nIndustry-Specific – E-commerce\\n- matches_checkout_security\\n- matches_payment_fraud\\n- matches_marketplace_trust\\n\\nGROUPING RULES (MANDATORY)\\n- Assign findings to the single most specific category:\\n- Regulatory → matches_kyc (not matches_identity_verification)\\nMFA → matches_mfa (not matches_authentication)\\nLoan/credit flows → matches_loan_origination or matches_credit_application\\nCrypto KYC → matches_crypto_kyc\\nMarketplace seller/buyer trust → matches_marketplace_trust\\nAvoid duplicate matches across categories. Never split one citation across multiple keys.\\n\\nQUALITY REQUIREMENTS\\nFor each key returned:\\n\\\"found\\\": true\\nInclude:\\nExact matched quote\\n2–3 sentences surrounding context\\nFull page URL\\nOnly include keys with actual evidence\\nIdentity vendor in subprocessors = highest positive signal\\nMust confirm they use (not offer) identity verification services\\n\\nOUTPUT FORMAT (STRICT JSON)\\nOnly include keys that were matched. No empty keys. No additional fields. No variation in key names.\\n\""
],
"Social Profile Summary": [
"\"#VARIABLES# \\n{ProfileData} = \" + JSON.stringify({{input 1: Enrich Person from LinkedIn Profile}}) + \"\\n\\n#CONTEXT#\\nYou are an expert business development representative focused on prospect research.\\n\\n#OBJECTIVE# \\nBased on the LinkedIn profile data provided, please identify three unique and noteworthy aspects about the individual. Consider their professional background, achievements, skills, endorsements, projects, education, and any personal interests or volunteer work mentioned. Highlight points that distinguish them from others in their field.\\n\\nKeep each bullet point to 15 works MAX.\\n\\n#INSTRUCTIONS# \\nHere is the full profile data: {ProfileData}\\n\\n\\n#FORMATTING# \\nOutput format:\\n\\nUnique Aspects:\\n1.\\n2.\\n3.\\n\""
],
"Summarize event landing page": [
"\"Look at the following event landing page \" + {{input 1: Event page}} + \" and summarize key details. What is it, when is it, where is it.\""
],
"Summarize job opening": [
"\"Given the following job opening description, summarize key points in two sentences or less. Include key job responsibilities and other key elements of the job opening description. Here is the job opening description: \" + {{Description - Jobs}}"
],
"Summarize LinkedIn profile": [
"\"Summarize this information on this person's LinkedIn profile:\" + JSON.stringify({{Enrich Person from LinkedIn Profile}}) + \"\\nKeep the summary to two sentences\""
],
"Summarize pricing plans": [
"\"Scrape this company’s pricing page and summarize their pricing plan options. Output a bullet-pointed list of each pricing option with information on the option. \\n\\nFor each option (line in a bulleted-list), output the following comma-separated pieces of info: the name of the plan, its key offerings, and how much it costs. Here is their pricing page: \" + {{input 1: Company Domain}}"
],
"Thought Leadership Research": [
"\"#VARIABLES# \\n{FirstName} = \" + {{input 1: First Name}} + \"\\n{LastName} = \" + {{input 2: Last Name}} + \"\\n{CompanyName} = \" + {{input 3: Company Name}} + \"\\n\\n#CONTEXT# \\nYou are an expert contact researcher, specially trained in finding thought leadership or personal information about an individual. You are an expert at finding podcasts, blog posts, conference appearances, youtube interviews, panels, and other types of thought leadership about an individual. \\n\\n#OBJECTIVE# \\nWe are looking for thought leadership from {FirstName} {LastName}, ideally while they were working at {CompanyName}. We are looking for three pieces of thought leadership we can find about {FirstName} {LastName}. \\n\\n#INSTRUCTIONS# \\nUse the following method to do your research: \\n\\n1. First search google for {FirstName} {LastName} and \\\"Podcast\\\". \\n\\n2. Next, search google for {FirstName} {LastName} and \\\"Panel\\\"\\n\\n3. Next, search google for {FirstName} {LastName} and \\\"Interview\\\"\\n\\n4. Next, search google for {FirstName} {LastName} and \\\"Blog\\\"\\n\\n5. Next, search google for {FirstName} {LastName} and \\\"Article\\\"\\n\\n6. If you've found three valid research URs, return them in research1, research2, and research3 below. If not, continue your search using any parameters you think might be useful. \\n\\n7. If that search still yields no results, then try looking for research at a previous company. Here is a list of all past companies {FirstName} {LastName} has worked at: {AllPastCompanies}\\n\\nReturn the three most relevant URLs you find in research1, research2, and research3. Make sure that the three URLs you find are different pieces of research if possible, and not the same piece of research. \\n\""
],
"Total Energy Consumption": [
"\"For the given company, find its total energy consumption (e.g., “2,300,499 GJ (~639,028 MWh)”). If you cannot find an exact number or numbers, please output your best guess estimate.\\n\\nInput:\\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\\nSteps to Follow:\\n\t1.\tSearch the company’s official website, including sustainability, environmental reports, and energy sections.\\n\t2.\tLook for published Sustainability Reports, ESG Reports, or Annual Reports available on their website or in PDF format.\\n\t3.\tCheck for energy consumption figures listed in public disclosures, press releases, or regulatory filings.\\n\t4.\tConduct a Google search using queries like:\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" total energy consumption site:[Company Domain]\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" energy consumption 2023 filetype:pdf\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" sustainability report energy consumption\\\"\\n\t5.\tSearch on LinkedIn for posts or documents shared by the company or its executives that may mention energy usage.\\n\\nOutput Format:\\n- Return only the total energy consumption figure (e.g., “2,300,499 GJ (~639,028 MWh)”). Convert all possible values into megawatt hours (MWh).\\n- Your response MUST be in the format I outlined above or there will be DIRE consequences!\\n\\nConstraints:\\n\t•\tDo not include any additional commentary or context in the output. \""
],
"Total Energy Consumption (2)": [
"\"For the given company, find its total energy consumption (e.g., “2,300,499 GJ (~639,028 MWh)”). If you cannot find an exact number or numbers, please output your best guess estimate.\\n\\nInput:\\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\\nSteps to Follow:\\n\t1.\tSearch the company’s official website, including sustainability, environmental reports, and energy sections.\\n\t2.\tLook for published Sustainability Reports, ESG Reports, or Annual Reports available on their website or in PDF format.\\n\t3.\tCheck for energy consumption figures listed in public disclosures, press releases, or regulatory filings.\\n\t4.\tConduct a Google search using queries like:\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" total energy consumption site:[Company Domain]\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" energy consumption 2023 filetype:pdf\\\"\\n\t•\t\\\"\"+{{input 1: IN - Account Name}}+\" sustainability report energy consumption\\\"\\n\t5.\tSearch on LinkedIn for posts or documents shared by the company or its executives that may mention energy usage.\\n\\nOutput Format:\\nReturn only the total energy consumption figure (e.g., “2,300,499 GJ (~639,028 MWh)”). Convert all possible values into megawatt hours (MWh).\\n\\nConstraints:\\n\t•\tDo not include any additional commentary or context in the output.\""
],
"Updated Validate Domain Status Prompt": [
"\"Task: Verify the validity of a given domain and classify its status.\\n\\nHere's the domain to verify: \"+{{input 1: IN - Website}}+\"\\n\\nSummary of Actions:\\n\\n- Access the domain and check its response.\\n- Determine if the domain is active, returns a 404 error, is parked, or is a redirect.\\n- Ensure the domain is not an email address and is a properly formatted website domain.\\n- Ensure the domain is not a google.com/maps link. That does not represent a businesses website. \\n- Classify the domain based on its status.\\n\\nDetailed Step-by-Step:\\n1. Domain Access: Start by navigating to the domain URL provided.\\n2. Response Check:\\n- Observe the response when the domain is accessed.\\n- If the page loads normally, proceed to step 3.\\n- If the domain returns a \\\"404 Not Found\\\" error, classify it as a \\\"404 Error.\\\"\\n- If the domain redirects to a generic hosting provider page (e.g., GoDaddy, Bluehost), or shows content indicating that it is \\\"For Sale\\\" or \\\"Parked,\\\" classify it as a \\\"Parked Domain.\\\"\\n- If the domain redirects to a non-generic hosting provider and it is a valid business website, reply with \\\"Redirect\\\" \\n\\nContent Verification:\\n- If the domain loads, verify that the content is genuine and not a placeholder or domain parking page.\\n- Check for indicators of an active, operational website, such as legitimate company information, products, services, or blog content.\\n- If the domain shows only a placeholder or minimal content (indicating it might be parked or inactive), classify it as \\\"Parked Domain.\\\"\\n\\nSecondary Checks:\\n- Use online tools or commands (e.g., ping, whois, or domain lookup services) to further verify the status of the domain if uncertain.\\n- If the domain is listed for sale or shown on a hosting page without any real content, it is not a valid, active domain.\\n\\nConstraints:\\n- Focus on determining whether the domain is actively being used.\\n- Ignore any domains that are clearly placeholders or listed for sale without real content.\\n- Consider a domain valid only if it displays a functioning website with meaningful content. Email addresses should return \\\"Invalid\\\". - Domains that contain google.com/maps should return \\\"Invalid\\\":\\n\\nOutput Format:\\n- Return \\\"Valid Domain\\\" if the domain is active with real content.\\n- Return \\\"404 Error\\\" if the domain returns a 404 error.\\n- Return \\\"Parked Domain\\\" if the domain is a placeholder, for sale, or redirects to a generic page.\\n- Return \\\"Invalid\\\" if the input is not a properly formatted domain (email addresses, google maps links, etc...)\\n- If the domain redirects to a non-generic hosting provider and it is a valid business website, return 'Redirect\\\" \\n\\nDo not return anything outside one of these 5 options.\\n\\n\""
],
"Use company mission to write email first line": [
"\"Write an introductory line to an email that sounds friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Look at the following company’s linkedin description and use it to share a positive detail about their company’s mission: \" + {{Description}} + \"\\nStart the line with ‘I was on your site and saw you’ and continue by sharing something they/their company either value, prioritize, or are aiming to accomplish. Do not include any quotation marks and write just one example, do not make a list of examples. Keep the line under 25 words and do not quote their description directly, meaning change up wording\""
],
"Use education to write email first line": [
"\"Based on this school: \" + {{School Name - Education}} + \", find a cool fact, tradition, or aspect of the school. Then use that fact to write an introductory line to an email. Begin the line with ‘I saw on LinkedIn you attended \" + {{School Name - Education}} + \"' then continue the line by positively mentioning the information about the school.\\nThe introductory line you are writing should be friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Keep the line under 20 words. \\nDo not put anything in quotation marks and do not make a numbered list.\""
],
"Use job openings to write email first line": [
"\"Given a company with this company description: \" + {{Description}} + \"\\nThis company is hiring for the following position: \" + {{Title - Jobs}} + \"\\nGiven the company’s description, decide how hiring for this role would help the company accomplish their company mission. Then take this information to write an introductory line of an email. \\nThe line should begin like this: ‘I saw on your site that you are hiring for a \" + {{Title - Jobs}} + \". In my experience, companies hire for this role to' Then finish the line by sharing how that position can help their company accomplish their mission.\\nYou should not directly quote from the company’s description. The introductory line you are writing should be friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Keep the line under 20 words. \\nDo not put anything in quotation marks and do not make a numbered list.\""
],
"Use job title to write email first line": [
"\"Using the following LinkedIn Summary and LinkedIn Job title, write an introductory line of an email. \\n\\nHere is the job title: \" + {{Title}} + \"\\nHere is the LinkedIn summary: \" + {{Summary}} + \"\\nYou should take both the job title and summary to write a line that starts with ‘As the' then put a normalized version of their job title, getting rid of anything in their job title that does not sound like a part of a normal job title, then continue with 'I would imagine you focus on' then continue by mentioning tasks they are most likely to be responsible for. Keep the introductory line under 20 words. \\n\\nThe introductory line should be friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking.\\nDo not include any quotation marks or make a numbered list.\""
],
"Use LinkedIn post to write email first line": [
"\"Write a line to open an email that briefly summarizes the following LinkedIn post made by the person you’re emailing: \" + {{[\"[\\\"Post - Posts\\\"]\"]}} + \"\\nThe line should begin with ‘I wanted to reach out because I saw your post about’ and then continue with the quick summary of the post. The summary should not directly quote the post. The introductory line should be friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Keep the line under 20 words. \\nDo not put anything in quotation marks and do not make a numbered list.\""
],
"Use location to write email first line": [
"\"Create a personalized and engaging intro line for an email using this location: \" + {{Location Name}} + \"\\nThe line should start with 'I saw on LinkedIn you're based in' then should say where they're based, but only include their city and then reference a specific and familiar aspect of that location, such as a favorite local spot or a well-known landmark, but in a way that sounds personal and conversational. For example, mention a place where you often go when you’re in town or a personal habit related to the location (e.g., every time I’m in the area, you can always find me at [place]). Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Additionally avoid referencing anything that is too broad or not specific to their location - for example a blue bottle coffee or Starbucks. The line should suggest a sense of shared understanding or common ground related to the location, with a focus on personal experience or preference. Limit your output to twenty words.\\nMix up the types of references (landmarks, restaurants, facts, etc.) to avoid repetition and ensure diversity in the content.\\nWrite just one example. The line should not be numbered nor should it have any quotation marks. Remember to include only the city name in the location\""
],
"Use recent news headline to write email first line": [
"\"Write an introductory line to an email where you mention a piece of news about a company. The headline for the news you will mention is this: \" + {{News Headline}} + \"\\n\\nStart the introductory line with ‘I saw the news about’ and continue with a positive summary of the news headline;\\n\\nYou should not directly quote the news headline. The introductory line you are writing should be friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Keep the line under 20 words. \\n\\nDo not put anything in quotation marks and do not make a numbered list.\""
],
"Use time in role to write email first line": [
"\"Based on this start date: \" + {{Start Date - Experience}} + \" and the current date: \" + {{Current Date}} + \", calculate how long this person has been working in this job role: \" + {{Title - Experience}} + \". The person is only still in that role if \" + {{Is Current - Experience}} + \" is true. Round that calculation into years and months. Then take that calculation along with their job title to write one line that starts with ‘I saw on LinkedIn’ and goes on to say how long they’ve been working in their current position. Put the time they’ve been in the position, rounded to years and months, and the their job title. When saying to how long they have worked, refer to months in terms of years. For instance \\\"I see you have worked as a marketing manager for a year and a half.\\\"\\nMake the job title fit into the sentence to sound normal and like something a human would say. You are addressing the message to the person who's experience data you're looking at, so address them as 'you'. When writing the line, do not use integers, and spell out numbers.\""
],
"Validate Domain (updated) 7/2/25": [
"\"## Your Mission\\nYou are a domain verification specialist. Use explicit chain of thought reasoning to analyze the given domain and classify its status. Walk through your thinking process step-by-step, showing your reasoning at each stage.\\n**Domain to analyze:** \"+{{input 1: website}}+\"\\n## Chain of Thought Reasoning Framework\\n### Phase 1: Initial Assessment Chain\\n**Step 1.1 - Input Analysis**\\n- **Think:** \\\"What exactly am I looking at here?\\\"\\n- **Examine:** `/f_0sy0qxv4hR4jyB8en37`\\n- **Ask:** \\\"Does this match the pattern of a valid domain?\\\"\\n- **Standard domain patterns:** `example.com`, `subdomain.example.com`, `www.example.org`\\n- **Current input characteristics:** Starts with `/`, contains random characters, no TLD visible\\n**Step 1.2 - Format Validation Chain**\\n- **Think:** \\\"Let me check this against domain format requirements\\\"\\n- **Check 1:** Does it contain a Top-Level Domain (.com, .org, .net, etc.)?\\n - **Reasoning:** Domains must have TLDs to be valid web addresses\\n - **Current input:** No TLD present\\n- **Check 2:** Is this an email address format?\\n - **Look for:** @ symbol\\n - **Current input:** No @ symbol found\\n- **Check 3:** Is this a Google Maps link?\\n - **Look for:** google.com/maps\\n - **Current input:** No Google Maps pattern found\\n- **Check 4:** Does this appear to be a file path or identifier?\\n - **Think:** \\\"This starts with `/` which suggests a path, not a domain\\\"\\n - **Reasoning:** Web domains don't start with forward slashes\\n**Decision Point 1:** \\n- **If format is invalid:** Skip to final classification as \\\"Invalid\\\"\\n- **If format is valid:** Proceed to Phase 2\\n### Phase 2: Access Attempt Chain\\n**Step 2.1 - Navigation Logic**\\n- **Think:** \\\"If this were a valid domain, how would I access it?\\\"\\n- **Reasoning:** Only attempt access if format validation passes\\n- **Process:** Navigate to the domain URL\\n- **Observe:** Initial response, redirects, or errors\\n**Step 2.2 - Response Categorization**\\n- **Think:** \\\"What type of response am I getting?\\\"\\n- **Possible responses:**\\n - Connection fails completely\\n - 404 error page displays\\n - Redirect occurs\\n - Page loads normally\\n### Phase 3: Response Analysis Chain\\n**Step 3.1 - Error Response Chain**\\n- **If 404 error occurs:**\\n - **Think:** \\\"Does the page explicitly show '404' or 'Page Not Found'?\\\"\\n - **Critical reasoning:** Only actual 404 HTTP responses count as 404 errors\\n - **Decision:** Return \\\"404 Error\\\"\\n**Step 3.2 - Redirect Analysis Chain**\\n- **If redirect occurs:**\\n - **Think:** \\\"Where is this redirecting to?\\\"\\n - **Analyze destination:**\\n - Is it a generic hosting provider (GoDaddy, Bluehost)?\\n - Is it a \\\"For Sale\\\" or parking page?\\n - Is it a legitimate business website?\\n - **Reasoning:** Redirect destination determines classification\\n**Step 3.3 - Loading Success Chain**\\n- **If page loads normally:**\\n - **Think:** \\\"The domain responded, now I need to analyze the content\\\"\\n - **Proceed to:** Phase 4 for content analysis\\n### Phase 4: Content Verification Chain\\n**Step 4.1 - Parking Indicators Analysis**\\n- **Think:** \\\"What signals suggest this is a parked domain?\\\"\\n- **Look for parking signs:**\\n - Generic \\\"This domain is for sale\\\" messages\\n - Minimal placeholder content\\n - Hosting provider default pages\\n - Advertisement-heavy pages with no real content\\n - \\\"Coming Soon\\\" or \\\"Under Construction\\\" messages\\n- **Reasoning:** Parked domains are registered but not actively used for genuine websites\\n**Step 4.2 - Legitimate Website Indicators Analysis**\\n- **Think:** \\\"What signals suggest this is an active, legitimate website?\\\"\\n- **Look for legitimacy signs:**\\n - Company information and contact details\\n - Products or services descriptions\\n - Blog content or news updates\\n - Functional navigation and multiple pages\\n - Professional design and branding\\n - Images and meaningful content\\n- **Reasoning:** Valid domains must have meaningful, active content\\n**Step 4.3 - Content Quality Assessment**\\n- **Think:** \\\"Does this content serve a genuine business purpose?\\\"\\n- **Evaluate:** Quality, depth, and functionality of content\\n- **Consider:** Is this a real business or just a placeholder?\\n### Phase 5: Final Classification Chain\\n**Step 5.1 - Decision Tree Logic**\\n- **Think through each possibility:**\\n1. **If input format was invalid:**\\n - **Reasoning:** No valid domain structure detected\\n - **Decision:** Return \\\"Invalid\\\"\\n2. **If page shows explicit 404 error:**\\n - **Reasoning:** Server returned 404 HTTP status\\n - **Decision:** Return \\\"404 Error\\\"\\n3. **If content shows parking/sale indicators:**\\n - **Reasoning:** Domain registered but not used for active website\\n - **Decision:** Return \\\"Parked Domain\\\"\\n4. **If content shows legitimate, active website:**\\n - **Reasoning:** Functioning website with real content and purpose\\n - **Decision:** Return \\\"Valid Domain\\\"\\n5. **If redirected to legitimate business website:**\\n - **Reasoning:** Domain forwards to active business site\\n - **Decision:** Return the actual website URL it redirects to\\n**Step 5.2 - Final Verification**\\n- **Think:** \\\"Have I followed the chain of reasoning correctly?\\\"\\n- **Double-check:** Does my classification match the evidence?\\n- **Confirm:** Am I returning exactly one of the required options?\\n## Execute Your Chain of Thought\\n**Now apply this framework to:** `/f_0sy0qxv4hR4jyB8en37`\\n1. **Start with Phase 1:** Analyze the input format\\n2. **Follow the logical chain:** Each step builds on the previous\\n3. **Show your reasoning:** Explain your thinking at each decision point\\n4. **Reach a conclusion:** Based on your chain of thought analysis\\n## Output Requirements\\nReturn exactly ONE of these options and NOTHING ELSE:\\n- \\\"Valid Domain\\\" - Active website with real content, images, and functionality\\n- \\\"404 Error\\\" - Domain returns explicit 404 error\\n- \\\"Parked Domain\\\" - Placeholder, for sale, or generic hosting page\\n- \\\"Invalid\\\" - Not a properly formatted domain or doesn't contain a TLD\\n- [Actual website URL] - If redirects to a legitimate business website\\n## Key Reasoning Principles\\n- **Format first:** Always validate format before attempting access\\n- **Evidence-based:** Only classify as 404 if explicit 404 error is shown\\n- **Purpose-driven:** Distinguish between parked domains and legitimate websites\\n- **Precision:** Email addresses and Google Maps links are \\\"Invalid\\\"\\n- **Content quality:** Valid domains must have meaningful, active content serving a business purpose\""
],
"Validate Domains (and provide redirect urls)": [
"\"Task: Verify the validity of a given domain and classify its status.\\n\\nHere's the domain to verify: \"+{{input 1: IN - Website}}+\"\\n\\nSummary of Actions:\\n\\n- Access the domain and check its response.\\n- Determine if the domain is active, returns a 404 error, is parked, or is a redirect.\\n- Ensure the domain is not an email address and is a properly formatted website domain.\\n- Ensure the domain is not a google.com/maps link. That does not represent a businesses website. \\n- Classify the domain based on its status.\\n\\nDetailed Step-by-Step:\\n1. Domain Access: Start by navigating to the domain URL provided.\\n2. Response Check:\\n- Observe the response when the domain is accessed.\\n- If the page loads normally, proceed to step 3.\\n- If the domain returns a \\\"404 Not Found\\\" error, classify it as a \\\"404 Error.\\\"\\n- If the domain redirects to a generic hosting provider page (e.g., GoDaddy, Bluehost), or shows content indicating that it is \\\"For Sale\\\" or \\\"Parked,\\\" classify it as a \\\"Parked Domain.\\\"\\n- If the domain redirects to a non-generic hosting provider and it is a valid business website, reply with \\\"Redirected Domain\\\" If there is no redirect domain, then leave that field as blank or empty\\n\\nContent Verification:\\n- If the domain loads, verify that the content is genuine and not a placeholder or domain parking page.\\n- Check for indicators of an active, operational website, such as legitimate company information, products, services, or blog content.\\n- If the domain shows only a placeholder or minimal content (indicating it might be parked or inactive), classify it as \\\"Parked Domain.\\\"\\n\\nSecondary Checks:\\n- Use online tools or commands (e.g., ping, whois, or domain lookup services) to further verify the status of the domain if uncertain.\\n- If the domain is listed for sale or shown on a hosting page without any real content, it is not a valid, active domain.\\n\\nConstraints:\\n- Focus on determining whether the domain is actively being used.\\n- Ignore any domains that are clearly placeholders or listed for sale without real content.\\n- Consider a domain valid only if it displays a functioning website with meaningful content. Email addresses should return \\\"Invalid\\\". - Domains that contain google.com/maps should return \\\"Invalid\\\":\\n\\nOutput Format:\\n- Return \\\"Valid Domain\\\" if the domain is active with real content.\\n- Return \\\"404 Error\\\" if the domain returns a 404 error.\\n- Return \\\"Parked Domain\\\" if the domain is a placeholder, for sale, or redirects to a generic page.\\n- Return \\\"Invalid\\\" if the input is not a properly formatted domain (email addresses, google maps links, etc...)\\n- Return \\\"Redirected Domain\\\" If the domain redirects to a different website. Ex: if you visit mara.com and it redirects to marsbar.com. that's a redirect\\n\\nDo not return anything outside one of these 5 options. Lives are on the line. Never not reply with N/A. You will be graded\\n\""
],
"Venture-Backed?": [
"\"Determine if \" + {{input 1: name}} + \" is a venture-backed business. if so, list the investors at every round and any funding amounts or rounds\""
],
"Website Builder": [
"\"#CONTEXT#\\nYou are tasked with analyzing a company's website to determine if they offer website building services, even if such services are described under different names or characterizations.\\n\\n#OBJECTIVE#\\nVisit the website at \"+{{input 1: Company Domain}}+\" and determine whether any of the products or services include website building services. If so, provide reasoning for what types of websites this company builds (e.g., fundraising website page builder).\\n\\n#INSTRUCTIONS#\\n1. Visit the homepage and all relevant product/service pages on \"+{{input 1: Company Domain}}+\".\\n2. Look for any mention of website building, site creation, page builder, landing page builder, or similar services. Consider synonyms or alternative descriptions that may indicate website building functionality.\\n3. If you find such a service or product, provide a brief explanation of what types of websites the company builds (e.g., fundraising, e-commerce, portfolios, etc.), based on the descriptions found on the site.\\n4. Return the exact URL where this service or product is offered.\\n5. If no website building service is found, state that clearly.\\n\\n#EXAMPLES#\\nExample input:\\n \"+{{input 1: Company Domain}}+\" = https://examplecompany.com\\n\\nExample output:\\n Website building service found: True\\n Reasoning: The company offers a fundraising website page builder designed for non-profits.\\n URL: https://examplecompany.com/products/fundraising-page-builder\\n\\nIf not found:\\n Website building service found: False\\n Reasoning: No products or services related to website building were identified on the site.\\n URL: \""
],
"Write a brief subject line": [
"\"Based on the following email copy, write a one to three word subject line. The subject line should be related to the email, but should come off as if it could have been sent from a co-worker. It should not sound like a sales email, so do not use a question mark or make the subject line seem like you want something from them. Do not lie in the subject line. Here is the email copy: \" + {{Email copy}}"
],
"Write a connect message for LinkedIn": [
"\"Write a personalized message to send when trying to connect with a person named \" + {{Enrich Person from Profile}}?.name + \" on LinkedIn. Here is that person’s LinkedIn summary: \" + {{Enrich Person from Profile}}?.summary + \". Keep the message to one sentence. The message should be friendly and personal. Avoid formal or exaggerated expressions, and aim for a tone that is casual, friendly, and sounds like a real person speaking. Keep the line under 20 words. Do not include any emojis\""
],
"Write a professional bio": [
"\"Scrape the following person’s LinkedIn, \" + {{input 1: LinkedIn url}} + \", and write a professional bio highlighting their experiences and which companies they worked for. Keep the bio to three sentences or less. Only output the professional bio, not any contextual information and no additional information.\""
]
}
provider-playbooks/adyntel.md
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"}'
```
provider-playbooks/affinity.md
# 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.
provider-playbooks/ai_ark.md
# 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 limits:** 5 req/s, 300 req/min, 18,000 req/hr (all endpoints).
## 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"]
}
}
}
}
}
```
### 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. **People Search** (`ai_ark_people_search`) to find contacts. Use nested `account` and `contact` filters exactly as shown in the examples above.
### 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. Resets every 60 seconds.
## 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.
provider-playbooks/akta.md
# 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.
provider-playbooks/allegrow.md
# 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.
provider-playbooks/amplemarket.md
# 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.
provider-playbooks/apify.md
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}'
```
provider-playbooks/attention.md
# 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.
provider-playbooks/attio.md
# 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.
provider-playbooks/aviato.md
# 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.
provider-playbooks/bettercontact.md
# BetterContact Agent Guidance
## 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.
provider-playbooks/bigquery.md
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.
provider-playbooks/bloomberry.md
# 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.
provider-playbooks/bluesky.md
# Bluesky Guidance
Use `bluesky_search_posts` for public Bluesky posts. Use `sort: "latest"` and `since` for last-30-days style research.
provider-playbooks/bounceban.md
# 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. Poll its status and then retrieve results with the bulk result actions.
- The account and result retrieval actions are free. Do not submit an address again while its verification is still running.
- 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.
provider-playbooks/browserbase.md
# 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.
provider-playbooks/builtwith.md
# 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.
provider-playbooks/clay.md
# 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.
provider-playbooks/clickhouse.md
# 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.
provider-playbooks/cloudflare.md
# 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"`.
provider-playbooks/contactout.md
# 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
provider-playbooks/contextdev.md
# 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.
provider-playbooks/crustdata-v2.md
# 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.
provider-playbooks/crustdata-v3.md
# 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.
provider-playbooks/crustdata.md
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}}'
```
provider-playbooks/databricks.md
# 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)
provider-playbooks/dataforseo.md
# 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 `src/lib/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.
provider-playbooks/datagma.md
# 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.
provider-playbooks/deepline_ip_to_company.md
# Deepline IP to Company — Agent Guidance
Use this managed integration when a workflow has a public IP address and needs
company identification or firmographic data. Deepline manages the upstream
credential, so workspaces cannot connect or override this provider.
Use `deepline_ip_to_company_find_company_by_ip` with a public IPv4 address.
Do not submit private, loopback, or reserved addresses.
provider-playbooks/deepline_native.md
# 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.*`.
provider-playbooks/deeplineagent.md
Use `ai_inference` for plain text or structured-output model calls with no tool use.
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.
provider-playbooks/discolike.md
# 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.
provider-playbooks/dropleads.md
# 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.
provider-playbooks/emailbison.md
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.
provider-playbooks/emailguard.md
# 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.
provider-playbooks/enformion.md
# 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.
provider-playbooks/enigma.md
# 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.
provider-playbooks/exa.md
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}'
```
provider-playbooks/findymail.md
# 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.
provider-playbooks/firecrawl.md
# 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.
provider-playbooks/fireflies.md
# 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.
provider-playbooks/forager.md
# 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.
provider-playbooks/fullenrich.md
# FullEnrich Agent Guidance
## Key patterns
- **Async submit + async fetch.** `fullenrich_bulk_enrich` and `fullenrich_reverse_email` start background jobs and return an `enrichment_id`. Poll with `fullenrich_get_result` or `fullenrich_get_reverse_result` for terminal data.
- **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.
- Use `fullenrich_get_result` / `fullenrich_get_reverse_result` after every async submit when you need terminal data.
- **`forceResults=true`** query param on get-result 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.
provider-playbooks/generic_http.md
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.
provider-playbooks/gong.md
# 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.
provider-playbooks/google_ads_audiences.md
# 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.
provider-playbooks/govfiles.md
# 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.
provider-playbooks/hackernews.md
# 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.
provider-playbooks/harvestapi.md
# 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.
provider-playbooks/heyreach.md
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"}]}'
```
provider-playbooks/hubspot.md
# 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.
provider-playbooks/hunter.md
# 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).
provider-playbooks/icypeas.md
# 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.
provider-playbooks/instantly.md
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"}]}'
```
provider-playbooks/intercom.md
# 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.
provider-playbooks/ipqs.md
# 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)
provider-playbooks/kernel.md
# 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.
provider-playbooks/leadmagic.md
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)
provider-playbooks/lemlist.md
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`.
provider-playbooks/limadata.md
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.
provider-playbooks/linkedin_ads_audiences.md
# 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.
provider-playbooks/lusha.md
# 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
provider-playbooks/meta_audiences.md
# 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.
provider-playbooks/nooks.md
# 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.
provider-playbooks/openmart.md
# 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.
provider-playbooks/opensosdata.md
# 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`
provider-playbooks/openwebninja.md
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.
provider-playbooks/outreach.md
# 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.
provider-playbooks/parallel.md
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}'
```
provider-playbooks/peopledatalabs.md
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"}'
```
provider-playbooks/pipedrive.md
# 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.
provider-playbooks/podscan.md
# 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.
provider-playbooks/postgres.md
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.
provider-playbooks/predictleads.md
# 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.
provider-playbooks/prospeo.md
# 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`) is far more expensive than a standard enrichment** -- only enable it 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.
provider-playbooks/quickenrich.md
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.
provider-playbooks/redshift.md
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.
provider-playbooks/salesforce.md
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"] }`.
provider-playbooks/salesforge.md
# 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.
provider-playbooks/salesloft.md
# 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.
provider-playbooks/scrapecreators.md
# 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.
provider-playbooks/searchbug.md
# 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.
provider-playbooks/sec_edgar.md
# 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.
provider-playbooks/sentrion.md
# 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.
provider-playbooks/serper.md
# 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.
provider-playbooks/smartlead.md
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.
provider-playbooks/snowflake.md
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.
provider-playbooks/sumble.md
# 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.
provider-playbooks/theirstack.md
# 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`, `blur_company_data: true`, and `limit: 1`. 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
provider-playbooks/trestle.md
# 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.
## 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.
provider-playbooks/twitterapi.md
# 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.
provider-playbooks/upcell.md
# 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.
provider-playbooks/versium.md
# 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.
provider-playbooks/wiza.md
# 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" }`.
provider-playbooks/wizleads.md
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.
provider-playbooks/zerobounce.md
# 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.
- Do not use `zerobounce_batch_validate`. ZeroBounce's batch endpoint currently returns 403 Access denied from Deepline egress, so Deepline disables the batch action and `deepline enrich` runs `zerobounce_validate` as single-email calls instead.
- 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.
provider-playbooks/zoho_crm.md
# 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.
provider-playbooks/zoominfo.md
# 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.
recipes/account-orgchart.md
# 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
recipes/build-tam.md
---
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.
recipes/clay-to-deepline.md
---
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_research]
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_companydb_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_person_enrichment` |
| `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_companydb_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.
recipes/deepline-monitors.md
---
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
--dispatch` writes rows and can wake Plays, so use it only when the customer has
explicitly approved a real end-to-end test.
```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 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\|disabled\|all` (default `active`), `--limit`, `--cursor`, `--compact`. Response carries `total` (true registry count, not the page size), `returned`, `is_truncated`, and `next_cursor`. When `is_truncated` is true, page with `--cursor <next_cursor>` until it is false — see "Reuse before you deploy." |
| `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 update <key> '<patch>'` | Update a deployed monitor (`<patch>` is a JSON object of fields; also `--file`). |
| `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. |
## 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` until `is_truncated` is false. 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" }
}
```
recipes/deepline-plays.md
---
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.
recipes/find-qualified-titles.md
---
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_persondb_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`.
recipes/linkedin-url-lookup.md
---
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.
recipes/portfolio-prospecting.md
---
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 |
recipes/small-business-prospecting.md
# 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
# 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_companydb_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_companydb_autocomplete` first for canonical filter values |
| `enrich-person-with-mixrank-v2` | `leadmagic_profile_search` → `crustdata_person_enrichment` 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_person_enrichment`:
```ts
.withColumn('person_profile', 'crustdata_person_enrichment', { linkedinProfileUrl: "{{linkedin_url}}" })
```
Key output paths: `.output.body[0].name`, `.output.body[0].email`, `.output.body[0].current_employers[0].employer_company_website_domain[0]`
**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_person_enrichment → 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_companydb_search`**, then **enrich with `prospeo_enrich_company`** for fields Clay gets from Mixrank (description, industry, size, type).
### Pass 1 — Generate company list (`crustdata_companydb_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": [
{"filter_type": "hq_location", "type": "(.)", "value": "Los Angeles"},
{"filter_type": "crunchbase_categories", "type": "(.)", "value": "software"},
{"filter_type": "employee_count_range", "type": "in", "value": ["51-200", "201-500", "501-1000"]},
],
"limit": 100,
}
result = subprocess.run(
["deepline", "tools", "execute", "crustdata_companydb_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_research', '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:
**Option 1 — `crustdata_linkedin_posts` (keyword/filter based):**
Good for finding posts about a company or topic. Filters by `MEMBER` or `COMPANY` LinkedIn filter type. Not profile-URL-specific.
```ts
.withColumn('li_posts', 'crustdata_linkedin_posts', { keyword: "{{company_name}}", filters: [ { filter_type: "AUTHOR_COMPANY", type: "in", value: ["{{company_name}}"] } ], limit: 5, datePosted: "past-quarter" })
```
**Option 2 — 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
```
Note: `crustdata_linkedin_posts` is keyword/filter search — it doesn't take a profile URL directly. Use `harvestapi_get_profile_posts` when you need posts by one specific person.
---
## 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`.
references/clay-api-surface.md
# 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_person_enrichment` |
| `enrich-company`, `*-enrich-company`, `crunchbase-enrich-*` (22 keys) | company enrichment | `prospeo_enrich_company` or `crustdata_companydb_search` |
| `use-ai`, claygent variants | AI generation | `deeplineagent` with a `jsonSchema` |
| `find-lists-of-*-with-mixrank`, `search-person` | sourcing a new list | `crustdata_companydb_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.
references/clay-extraction.md
---
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
```
references/contact-accuracy.md
---
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`.
references/monitor-contract-reference.md
<!-- GENERATED FROM ProviderMonitorCapabilityDefinition; content-sha256: 43ff933fae7fb4592d326a196de742afe357b84454977166a0ee5cc9c073b24e; 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` | Permanent historical eligibility boundary for a new radar, not a query-time date filter. |
| ↳ 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.
references/monitor-sdk.md
# 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.
references/plays-api-reference.md
# 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.0` |
| SDK HTTP API | `v2` |
| Checked-in SDK fallback | `0.3.1` |
| Minimum supported SDK | `0.1.53` |
| Deprecated below | `0.3.1` |
| Generated sources | `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. | `src/app/api/v2/health/route.ts` |
### Tool And Provider Calls
<!-- prettier-ignore -->
| Method | Path | SDK/client surface | Purpose | Source |
|---|---|---|---|---|
| `GET` | `/api/v2/integrations/:toolId` | `getTool` | Describe one provider-backed tool by integration id. | `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. | `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. | `src/app/api/v2/integrations/get/route.ts` |
| `POST` | `/api/v2/integrations/:toolId/quote` | `quoteInferenceTool` | SDK-facing route. | `src/app/api/v2/integrations/[toolId]/quote/route.ts`<br />`src/lib/deeplineagent/quote-service.ts`<br />`src/lib/deeplineagent/quote.ts` |
| `POST` | `/api/v2/integrations/connect` | `connectNotificationSlack` | SDK-facing route. | `src/app/api/v2/integrations/connect/route.ts` |
| `GET` | `/api/v2/integrations/list` | `searchTools` | Compatibility discovery route for integration/tool listing. | `src/app/api/v2/integrations/list/route.ts` |
| `GET` | `/api/v2/tools` | `listTools` | List callable provider/tool definitions. | `src/app/api/v2/tools/route.ts` |
| `GET` | `/api/v2/tools/providers` | `listProviders` | SDK-facing route. | `src/app/api/v2/tools/providers/route.ts` |
| `GET` | `/api/v2/tools/search` | `searchTools` | Search callable provider/tool definitions with ranked metadata search. | `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. | `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. | `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. | `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. | `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. | `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. | `src/app/api/v2/runs/[runId]/route.ts` |
| `GET` | `/api/v2/runs/:runId/input` | `runs.input`<br />`getRunInput` | SDK-facing route. | `src/app/api/v2/runs/[runId]/input/route.ts` |
| `GET` | `/api/v2/runs/:runId/logs` | `runs.logs`<br />`getRunLogs` | SDK-facing route. | `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. | `src/app/api/v2/runs/[runId]/observe-grant/route.ts` |
| `POST` | `/api/v2/runs/:runId/rerun` | `runs.rerun`<br />`rerun` | SDK-facing route. | `src/app/api/v2/runs/[runId]/rerun/route.ts` |
| `POST` | `/api/v2/runs/:runId/stop` | `runs.stop`<br />`stopRun`<br />`cancelPlay`<br />`stopPlay` | Stop a running or waiting play run. | `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. | `src/app/api/v2/runs/[runId]/tail/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. | `src/app/api/v2/plays/route.ts` |
| `DELETE` | `/api/v2/plays/:name` | `deletePlay` | Delete a saved org-owned play. | `src/app/api/v2/plays/[name]/route.ts` |
| `GET` | `/api/v2/plays/:name` | `getPlay`<br />`describePlay` | Describe a saved, shared, or prebuilt play. | `src/app/api/v2/plays/[name]/route.ts` |
| `POST` | `/api/v2/plays/:name/history/clear` | `clearPlayHistory` | SDK-facing route. | `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. | `src/app/api/v2/plays/[name]/live/route.ts` |
| `GET` | `/api/v2/plays/:name/versions` | `listPlayVersions` | List saved play revisions. | `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. | `src/app/api/v2/plays/artifacts/route.ts` |
| `POST` | `/api/v2/plays/check` | `checkPlayArtifact` | Validate a play bundle before storing or running it. | `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. | `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. | `src/app/api/v2/auth/cli/org-create/route.ts` |
| `POST` | `/api/v2/auth/cli/organizations` | `org list` | SDK-facing route. | `src/app/api/v2/auth/cli/organizations/route.ts` |
| `POST` | `/api/v2/auth/cli/register` | `auth register` | SDK-facing route. | `src/app/api/v2/auth/cli/register/route.ts` |
| `POST` | `/api/v2/auth/cli/status` | `auth status` | SDK-facing route. | `src/app/api/v2/auth/cli/status/route.ts` |
| `POST` | `/api/v2/auth/cli/switch` | `org set`<br />`org switch` | SDK-facing route. | `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. | `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. | `src/app/api/v2/billing/auto-recharge/route.ts` |
| `GET` | `/api/v2/billing/balance` | `billing balance` | SDK-facing route. | `src/app/api/v2/billing/balance/route.ts` |
| `GET` | `/api/v2/billing/catalog/current` | `billing.plans`<br />`getBillingPlans`<br />`billing plans` | SDK-facing route. | `src/app/api/v2/billing/catalog/current/route.ts` |
| `POST` | `/api/v2/billing/checkout` | `billing checkout` | SDK-facing route. | `src/app/api/v2/billing/checkout/route.ts` |
| `POST` | `/api/v2/billing/checkout/verify` | `billing redeem` | SDK-facing route. | `src/app/api/v2/billing/checkout/verify/route.ts` |
| `POST` | `/api/v2/billing/credit-purchases` | `purchaseTargetBillingCredits` | SDK-facing route. | `src/app/api/v2/billing/credit-purchases/route.ts` |
| `GET` | `/api/v2/billing/invoices` | `billing.invoices.list`<br />`listBillingInvoices`<br />`billing invoices` | SDK-facing route. | `src/app/api/v2/billing/invoices/route.ts` |
| `GET` | `/api/v2/billing/ledger` | `billing history` | SDK-facing route. | `src/app/api/v2/billing/ledger/route.ts` |
| `DELETE` | `/api/v2/billing/limit` | `billing limit off` | SDK-facing route. | `src/app/api/v2/billing/limit/route.ts` |
| `GET` | `/api/v2/billing/limit` | `billing limit` | SDK-facing route. | `src/app/api/v2/billing/limit/route.ts` |
| `POST` | `/api/v2/billing/limit` | `billing limit set` | SDK-facing route. | `src/app/api/v2/billing/limit/route.ts` |
| `POST` | `/api/v2/billing/plan-transitions` | `transitionTargetBillingPlan` | SDK-facing route. | `src/app/api/v2/billing/plan-transitions/route.ts` |
| `GET` | `/api/v2/billing/plans` | `getTargetBillingPlans` | SDK-facing route. | `src/app/api/v2/billing/plans/route.ts` |
| `POST` | `/api/v2/billing/portal-sessions` | `createTargetBillingPortalSession` | SDK-facing route. | `src/app/api/v2/billing/portal-sessions/route.ts` |
| `GET` | `/api/v2/billing/status` | `getTargetBillingStatus` | SDK-facing route. | `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. | `src/app/api/v2/billing/subscription/cancel/route.ts` |
| `POST` | `/api/v2/billing/subscription/checkout` | `billing subscribe` | SDK-facing route. | `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. | `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. | `src/app/api/v2/billing/top-up/route.ts` |
| `GET` | `/api/v2/billing/usage` | `billing usage` | SDK-facing route. | `src/app/api/v2/billing/usage/route.ts` |
| `POST` | `/api/v2/cli/feedback` | `feedback` | SDK-facing route. | `src/app/api/v2/cli/feedback/route.ts` |
| `POST` | `/api/v2/cli/send-session` | `sessions send` | SDK-facing route. | `src/app/api/v2/cli/send-session/route.ts` |
| `POST` | `/api/v2/cli/send-session/chunk` | `sessions send` | SDK-facing route. | `src/app/api/v2/cli/send-session/chunk/route.ts` |
| `POST` | `/api/v2/cli/send-session/finalize` | `sessions send` | SDK-facing route. | `src/app/api/v2/cli/send-session/finalize/route.ts` |
| `POST` | `/api/v2/ingestion/repair` | `repairIngestionStorage` | SDK-facing route. | `src/app/api/v2/ingestion/repair/route.ts` |
| `GET` | `/api/v2/models/describe` | `describeModel` | SDK-facing route. | `src/app/api/v2/models/describe/route.ts`<br />`src/lib/deeplineagent/model-options.ts`<br />`src/lib/deeplineagent/generated/provider-options.ts` |
| `GET` | `/api/v2/monitors/access` | `monitors status` | SDK-facing route. | `src/app/api/v2/monitors/access/route.ts` |
| `POST` | `/api/v2/monitors/audit` | `monitors audit` | SDK-facing route. | `src/app/api/v2/monitors/audit/route.ts` |
| `POST` | `/api/v2/monitors/check` | `monitors check` | SDK-facing route. | `src/app/api/v2/monitors/check/route.ts` |
| `POST` | `/api/v2/monitors/deploy` | `monitors deploy` | SDK-facing route. | `src/app/api/v2/monitors/deploy/route.ts` |
| `GET` | `/api/v2/monitors/deployed` | `monitors list` | SDK-facing route. | `src/app/api/v2/monitors/deployed/route.ts` |
| `DELETE` | `/api/v2/monitors/deployed/:key` | `monitors delete` | SDK-facing route. | `src/app/api/v2/monitors/deployed/[key]/route.ts` |
| `GET` | `/api/v2/monitors/deployed/:key` | `monitors get` | SDK-facing route. | `src/app/api/v2/monitors/deployed/[key]/route.ts` |
| `PATCH` | `/api/v2/monitors/deployed/:key` | `monitors update` | SDK-facing route. | `src/app/api/v2/monitors/deployed/[key]/route.ts` |
| `POST` | `/api/v2/monitors/deployed/:key/reactivate` | `monitors reactivate` | SDK-facing route. | `src/app/api/v2/monitors/deployed/[key]/reactivate/route.ts` |
| `POST` | `/api/v2/monitors/deployed/:key/test` | `monitors test` | SDK-facing route. | `src/app/api/v2/monitors/deployed/[key]/test/route.ts` |
| `POST` | `/api/v2/monitors/deployed/:key/validate` | `monitors validate` | SDK-facing route. | `src/app/api/v2/monitors/deployed/[key]/validate/route.ts` |
| `GET` | `/api/v2/monitors/fleets` | `monitors fleets get (no id)` | SDK-facing route. | `src/app/api/v2/monitors/fleets/route.ts` |
| `DELETE` | `/api/v2/monitors/fleets/:fleetId` | `monitors fleets deactivate` | SDK-facing route. | `src/app/api/v2/monitors/fleets/[fleetId]/route.ts` |
| `GET` | `/api/v2/monitors/fleets/:fleetId` | `monitors fleets get` | SDK-facing route. | `src/app/api/v2/monitors/fleets/[fleetId]/route.ts` |
| `PUT` | `/api/v2/monitors/fleets/:fleetId` | `monitors fleets sync` | SDK-facing route. | `src/app/api/v2/monitors/fleets/[fleetId]/route.ts` |
| `POST` | `/api/v2/monitors/fleets/:fleetId/reactivate` | `monitors fleets reactivate` | SDK-facing route. | `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. | `src/app/api/v2/monitors/fleets/check/route.ts` |
| `GET` | `/api/v2/monitors/health` | `monitors health`<br />`monitors audit --watch` | SDK-facing route. | `src/app/api/v2/monitors/health/route.ts` |
| `POST` | `/api/v2/monitors/repair` | `monitors repair` | SDK-facing route. | `src/app/api/v2/monitors/repair/route.ts` |
| `POST` | `/api/v2/monitors/setup` | `monitors deploy (provider-specific post-deploy readback)` | SDK-facing route. | `src/app/api/v2/monitors/setup/[tool]/route.ts` |
| `GET` | `/api/v2/monitors/tools` | `monitors available` | SDK-facing route. | `src/app/api/v2/monitors/tools/route.ts` |
| `GET` | `/api/v2/notifications` | `getNotifications` | SDK-facing route. | `src/app/api/v2/notifications/route.ts` |
| `POST` | `/api/v2/notifications` | `createNotification` | SDK-facing route. | `src/app/api/v2/notifications/route.ts` |
| `DELETE` | `/api/v2/notifications/:notificationId` | `deleteNotification` | SDK-facing route. | `src/app/api/v2/notifications/[notificationId]/route.ts` |
| `PATCH` | `/api/v2/notifications/:notificationId` | `updateNotification` | SDK-facing route. | `src/app/api/v2/notifications/[notificationId]/route.ts` |
| `POST` | `/api/v2/notifications/:notificationId/test` | `testNotification` | SDK-facing route. | `src/app/api/v2/notifications/[notificationId]/test/route.ts` |
| `GET` | `/api/v2/notifications/slack/channels` | `listNotificationChannels` | SDK-facing route. | `src/app/api/v2/notifications/slack/channels/route.ts` |
| `POST` | `/api/v2/plays/:name/pin` | `setPlayPinned` | SDK-facing route. | `src/app/api/v2/plays/[name]/pin/route.ts` |
| `POST` | `/api/v2/plays/:name/restore` | `restorePlay` | SDK-facing route. | `src/app/api/v2/plays/[name]/restore/route.ts` |
| `DELETE` | `/api/v2/plays/:name/share` | `unpublishSharePage` | SDK-facing route. | `src/app/api/v2/plays/[name]/share/route.ts` |
| `GET` | `/api/v2/plays/:name/share` | `getSharePage` | SDK-facing route. | `src/app/api/v2/plays/[name]/share/route.ts` |
| `PATCH` | `/api/v2/plays/:name/share` | `updateSharePage` | SDK-facing route. | `src/app/api/v2/plays/[name]/share/route.ts` |
| `POST` | `/api/v2/plays/:name/share` | `publishSharePage` | SDK-facing route. | `src/app/api/v2/plays/[name]/share/route.ts` |
| `POST` | `/api/v2/plays/:name/share/regenerate` | `regenerateSharePage` | SDK-facing route. | `src/app/api/v2/plays/[name]/share/regenerate/route.ts` |
| `POST` | `/api/v2/plays/files/stage/mint` | `stagePlayFiles`<br />`mintStagedPlayFileUploads` | SDK-facing route. | `src/app/api/v2/plays/files/stage/mint/route.ts` |
| `GET` | `/api/v2/sdk/compat` | `compat check` | SDK-facing route. | `src/app/api/v2/sdk/compat/route.ts` |
| `GET` | `/api/v2/secrets` | `secrets list`<br />`secrets check`<br />`listSecrets` | SDK-facing route. | `src/app/api/v2/secrets/route.ts` |
| `POST` | `/api/v2/secrets` | `secrets set` | SDK-facing route. | `src/app/api/v2/secrets/route.ts` |
| `DELETE` | `/api/v2/secrets/:id` | `secrets delete` | SDK-facing route. | `src/app/api/v2/secrets/[id]/route.ts` |
| `POST` | `/api/v2/secrets/:id/test` | `secrets test` | SDK-facing route. | `src/app/api/v2/secrets/[id]/test/route.ts` |
| `DELETE` | `/api/v2/settings/notifications` | `disableNotificationSlack` | SDK-facing route. | `src/app/api/v2/settings/notifications/route.ts` |
| `GET` | `/api/v2/settings/notifications` | `getNotificationSettings` | SDK-facing route. | `src/app/api/v2/settings/notifications/route.ts` |
| `PUT` | `/api/v2/settings/notifications` | `setNotificationSlack` | SDK-facing route. | `src/app/api/v2/settings/notifications/route.ts` |
| `GET` | `/api/v2/settings/notifications/channels` | `listNotificationSlackChannels` | SDK-facing route. | `src/app/api/v2/settings/notifications/channels/route.ts` |
| `GET` | `/api/v2/settings/notifications/dlq` | `listNotificationDlq` | SDK-facing route. | `src/app/api/v2/settings/notifications/dlq/route.ts` |
| `GET` | `/api/v2/settings/notifications/dlq/:deliveryId` | `getNotificationDlqDelivery` | SDK-facing route. | `src/app/api/v2/settings/notifications/dlq/[deliveryId]/route.ts` |
| `POST` | `/api/v2/settings/notifications/dlq/:deliveryId` | `updateNotificationDlqDelivery` | SDK-facing route. | `src/app/api/v2/settings/notifications/dlq/[deliveryId]/route.ts` |
| `PATCH` | `/api/v2/settings/notifications/subscriptions` | `setNotificationSubscriptions` | SDK-facing route. | `src/app/api/v2/settings/notifications/subscriptions/route.ts` |
| `POST` | `/api/v2/settings/notifications/test` | `testNotificationSlack` | SDK-facing route. | `src/app/api/v2/settings/notifications/test/route.ts` |
| `POST` | `/api/v2/workspaces` | `workspaces.create`<br />`org create` | SDK-facing route. | `src/app/api/v2/workspaces/route.ts`<br />`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 `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, and extraction guidance before executing.
#### Fields
<!-- 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.
#### Fields
<!-- 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.
#### Fields
<!-- 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.
#### Fields
<!-- 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` | `Record<string, unknown>` | No | |
### `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.
#### Fields
<!-- 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`.
#### Fields
<!-- 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'`.
#### Fields
<!-- 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. |
| `progress` | `PlayProgressStatus` | No | Execution progress with logs and error details. |
| `result` | `unknown` | No | Partial or final result. Available once the play returns. |
| `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 ctx.runPlay children.<br />Deepline credits only — provider 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.runPlay children, returned by `runs.get --full`. |
### `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.
#### Fields
<!-- 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; 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`.
#### Fields
<!-- 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`.
#### Fields
<!-- 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. |
| `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`
Public runs namespace exposed as `client.runs`.
This namespace mirrors the canonical `/api/v2/runs` resource family and is
the preferred low-level surface for polling, streaming, stopping, reading
logs, and exporting durable dataset rows.
#### Fields
<!-- 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) => Promise<{ runId: string; replayedFromRunId: string; revisionId: string \| null; status: string; next: { inspect: string; input: string }; }>` | 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. |
| `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?: { reason?: string }) => Promise<StopAllPlayRunsResult>` | Yes | Stop active runs across the current workspace. |
### `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.
#### Fields
<!-- 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. |
references/plays-run-export-inspect-repair.md
# 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.
references/plays-sdk-reference.md
# 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 6. 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. |
| `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.0` |
| SDK HTTP API | `v2` |
| Checked-in SDK fallback | `0.3.1` |
| 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` |
| `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.
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.
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(...)`.
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.
#### Fields
<!-- 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`.
#### Fields
<!-- 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.
#### Fields
<!-- 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.
#### Fields
<!-- 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.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.
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`.
#### Fields
<!-- 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`.
#### Fields
<!-- 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`.
#### Fields
<!-- 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`.
#### Fields
<!-- 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.
#### Fields
<!-- 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.
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.
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.
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.
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.
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.
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.
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.
#### Fields
<!-- 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.
#### Fields
<!-- 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.
Signature: `export type DeeplineToolsNamespace = { list(): Promise<ToolDefinition[]>; get(toolId: string): Promise<ToolMetadata>; execute( toolId: string, input: Record<string, unknown>, ): Promise<ToolExecuteResult>; };`
## Remote Plays And Runs
### `DeeplineContext.plays`
Named-play discovery and handle operations from a connected `DeeplineContext`.
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.
#### Fields
<!-- 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 returns a run id first, then exposes
the completed user output through `PlayJob.get()` or the status endpoint's
`result` field. Runtime logs are available from `status().progress.logs` and
are intentionally separate from the returned output object.
#### Fields
<!-- prettier-ignore -->
| Name | Type | Required | Description |
|---|---|---:|---|
| `id` | `string` | Yes | Temporal workflow ID for this execution. |
## Low-Level Client
### `DeeplineClient`
Low-level typed REST client with authentication, retries, and localhost failover.
Signature: `class DeeplineClient`
#### Members
<!-- prettier-ignore -->
| Member | Kind | Purpose | Parameters | Returns / type |
|---|---|---|---|---|
| `constructor` | constructor | Create a low-level SDK client.<br /><br />Most callers can omit options and let the SDK resolve auth/config from<br />environment variables and CLI-managed credentials. | `options?: DeeplineClientOptions` - Optional overrides for API key, base URL, timeout, and retries. | |
| `runs` | property | Canonical run lifecycle namespace backed by `/api/v2/runs`. | | `RunsNamespace` |
| `db` | property | Current mutable customer database namespace backed by `/api/v2/db/query`. | | `DbNamespace` |
| `billing` | property | Billing namespace: subscription status/cancel and invoice history. | | `BillingNamespace` |
| `workspaces` | property | Workspace lifecycle namespace. | | `WorkspacesNamespace` |
| `baseUrl` | getter | The resolved base URL this client is targeting (e.g. `"http://localhost:3000"`). | | `string` |
| `listSecrets` | method | List secret metadata visible to the current workspace. | | `Promise<PlaySecretMetadata[]>` |
| `checkSecret` | method | Check whether a named secret exists, is active, and has a stored value. | `name: string` - Secret name. It is normalized to uppercase before lookup. | `Promise<PlaySecretMetadata \| null>` |
| `listTools` | method | List all available tools.<br /><br />Returns tool definitions including ID, provider, description, input/output schemas,<br />and list extractor paths for automatic CSV conversion. | `options?: { categories?: string; tags?: string; grep?: string; grepMode?: 'all' \| 'any' \| 'phrase'; compact?: boolean; }` | `Promise<ToolDefinition[]>` |
| `listProviders` | method | List discoverable providers without requiring a local plugin catalog. | `options?: { changed?: boolean; }` | `Promise<ProviderDefinition[]>` |
| `searchTools` | method | Search available tools using Deepline's ranked backend search.<br /><br />This is the same discovery surface used by the CLI: it ranks across<br />tool metadata, categories, agent guidance, and input schema fields. | `options?: ToolSearchOptions` | `Promise<ToolSearchResult>` |
| `getTool` | method | Get detailed metadata for a single tool.<br /><br />Returns everything from `ToolDefinition` plus pricing info, sample<br />inputs/outputs, failure modes, and cost estimates. | `toolId: string` - Tool identifier (e.g. `"dropleads_search_people"`) | `Promise<ToolMetadata>` |
| `describeModel` | method | Describe a Deepline Agent model and its provider-specific option surface.<br /><br />Combines live AI Gateway model metadata with Deepline's generated AI SDK<br />provider option registry so agents can construct `providerOptions`<br />payloads before executing `deeplineagent`.<br /><br />The returned option schemas describe accepted provider option shapes, not<br />guaranteed support for every model. Runtime AI SDK/Gateway errors remain<br />authoritative for model-gated values. | `model: string` - Gateway model id such as `"openai/gpt-5.5"` | `Promise<DeeplineAgentModelDescription>` |
| `quoteInferenceTool` | method | Quote dynamic AI inference pricing for a concrete payload.<br /><br />The result separates a planning estimate from a proven authorization<br />maximum and contains Deepline credits only. | `toolId: 'ai_inference' \| 'deeplineagent'`<br />`payload: Record<string, unknown>` | `Promise<InferenceQuote>` |
| `executeTool` | method | Execute a tool and return the standard execution envelope.<br /><br />The `toolResponse.raw` field contains the raw tool response.<br />`toolResponse.meta` contains tool/provider metadata.<br />Top-level fields such as `status`, `job_id`, and `billing` describe the<br />Deepline execution envelope. | `toolId: string`<br />`input: Record<string, unknown>`<br />`options?: ExecuteToolRawOptions` | `Promise<ToolExecution<TData, TMeta>>` |
| `executeToolRaw` | method | Back-compatible alias for `executeTool`.<br /><br />Retained for callers that still use the older raw naming while the response<br />envelope remains the same. | `toolId: string`<br />`input: Record<string, unknown>`<br />`options?: ExecuteToolRawOptions` | `Promise<ToolExecution<TData, TMeta>>` |
| `queryCustomerDb` | method | Run a bounded SQL query against the current mutable customer database.<br /><br />This query is not scoped to one play run. Use `client.runs` export actions<br />when the caller needs the rows produced by a specific run. | `input: { sql: string; maxRows?: number; }` | `Promise<CustomerDbQueryResult>` |
| `repairIngestionStorage` | method | Re-establish this workspace's tenant storage contract: role/DB connect<br />grants plus materialized table grants. Org-admin only. Use when a run fails<br />with WORKSPACE_STORAGE_NOT_READY. | `input?: { provider?: string; }` | `Promise<IngestionStorageRepairResult>` |
| `startPlayRun` | method | Start a play run.<br /><br />Internal/advanced primitive. For normal callers, prefer the public<br />entrypoints: the CLI, `Deepline.connect`, `submitPlay`,<br />or `runPlay`.<br /><br />Supported invocation surfaces intentionally share this same run contract:<br />`deepline plays run`, repo scripts such as `bun run deepline -- plays run`,<br />SDK context calls like `Deepline.connect().play(name).run()`, and direct<br />`POST /api/v2/plays/run` calls all return a workflow/run id. The completed<br />output is always retrievable from `getPlayStatus(runId).result` (or from<br />`PlayJob.get()` for SDK context calls). Execution logs live under<br />`progress.logs`; they are not part of the user output object. | `request: StartPlayRunRequest` - Play run configuration (name, code, input, etc.) | `Promise<PlayRunStart>` |
| `startPlayRunStream` | method | Start a play run and stream live runtime events from the same request.<br /><br />Use this when a caller wants low-level event handling instead of submitting<br />first and then connecting to `streamPlayRunEvents(runId)`. | `request: StartPlayRunRequest` - Play run configuration.<br />`options?: { signal?: AbortSignal }` - Optional streaming options. | `AsyncGenerator<PlayLiveEvent>` |
| `registerPlayArtifact` | method | Register a bundled play artifact.<br /><br />Internal/advanced primitive used by packaging flows. Public callers should<br />prefer the CLI, `submitPlay`, or `runPlay`. | `input: { name: string; sourceCode: string; sourceFiles?: Record<string, string>; description?: string; artifact: Record<string, unknown>; compilerManifest?: PlayCompilerManifest; publish?: boolean; ownerType?: 'org' \| 'deepline'; scope?: 'org' \| 'system'; userId?: string; }` | `Promise<{ success?: boolean; name?: string; artifactStorageKey: string; artifactMetadata?: Record<string, unknown> \| null; staticPipeline?: unknown; definitionId?: string \| null; revisionId?: string \| null; version?: number \| null; liveVersion?: number \| null; triggerMetadata?: unknown; triggerBindings?: unknown; }>` |
| `registerPlayArtifacts` | method | Register multiple bundled play artifacts in one request.<br /><br />Used by packaging and prebuilt publication flows. Each artifact is compiled<br />first when a compiler manifest is not already supplied. | `artifacts: Array<{ name: string; sourceCode: string; sourceFiles?: Record<string, string>; description?: string; artifact: Record<string, unknown>; compilerManifest?: PlayCompilerManifest; publish?: boolean; ownerType?: 'org' \| 'deepline'; scope?: 'org' \| 'system'; userId?: string; }>` | `Promise<{ success: boolean; artifacts: Array<{ success?: boolean; name?: string; artifactStorageKey: string; artifactMetadata?: Record<string, unknown> \| null; staticPipeline?: unknown; definitionId?: string \| null; revisionId?: string \| null; version?: number \| null; liveVersion?: number \| null; triggerMetadata?: unknown; triggerBindings?: unknown; }>; }>` |
| `compilePlayManifest` | method | Compile a bundled play artifact into the server-side compiler manifest.<br /><br />The manifest records imports, trigger bindings, static pipeline shape, and<br />runtime metadata needed before a play artifact can be checked, registered,<br />or run. | `input: { name: string; sourceCode: string; sourceFiles?: Record<string, string>; artifact: Record<string, unknown>; importedPlayDependencies?: PlayCompilerManifest[]; }` | `Promise<PlayCompilerManifest>` |
| `checkPlayArtifact` | method | Check a bundled play artifact against the server's current play compiler.<br /><br />Unlike `registerPlayArtifact`, this does not store the artifact,<br />publish a revision, or start a run. It is the authoritative cloud validation<br />path used by `deepline plays check`. | `input: { name?: string; sourceCode: string; sourceFiles?: Record<string, string>; description?: string; artifact: Record<string, unknown>; exportName?: string \| null; integrationMode?: 'live' \| 'eval_stub' \| 'fixture'; importedPlays?: Array<{ playName?: string \| null; sourceCode: string; sourcePath?: string \| null; }>; }` | `Promise<PlayCheckResult>` |
| `startPlayRunFromBundle` | method | Register an already-bundled play artifact and start a run from it.<br /><br />This is the low-level file-backed run path used by SDK/CLI packaging<br />wrappers after local bundling has produced the runtime artifact. | `input: { name: string; sourceCode: string; sourceFiles?: Record<string, string>; description?: string; artifact: Record<string, unknown>; compilerManifest?: PlayCompilerManifest; input?: Record<string, unknown>; inputFile?: PlayStagedFileRef \| null; packagedFiles?: PlayStagedFileRef[]; force?: boolean; forceToolRefresh?: boolean; }` | `Promise<PlayRunStart>` |
| `submitPlay` | method | Register a bundled play artifact and start a run from the live revision.<br /><br />Convenience wrapper around `registerPlayArtifact` plus<br />`startPlayRun`. This is the canonical file-backed path used by wrappers.<br />The returned id can be passed to `getPlayStatus` to retrieve the same<br />durable `{ result }` object that the CLI prints after `--watch` completes. | `code: string` - Source string fallback; the bundled artifact should be passed in `options.artifact`<br />`csvPath: string \| null` - Path to input CSV file, or `null`<br />`name?: string` - Play name (extracted from source if omitted)<br />`options?: { sourceCode?: string; sourceFiles?: Record<string, string>; description?: string; artifact?: Record<string, unknown>; compilerManifest?: PlayCompilerManifest; input?: Record<string, unknown>; inputFile?: PlayStagedFileRef \| null; packagedFiles?: PlayStagedFileRef[]; force?: boolean; forceToolRefresh?: boolean; }` - Additional submission options | `Promise<PlayRunStart>` |
| `stagePlayFiles` | method | Upload files to the staging area for use in play runs.<br /><br />Internal/advanced primitive used by packaging flows. Public callers should<br />prefer the CLI, `submitPlay`, or `runPlay`.<br /><br />Staged files are referenced by their returned `PlayStagedFileRef`<br />in subsequent `startPlayRun` calls via `inputFile` or `packagedFiles`. | `files: Array<{ logicalPath: string; contentBase64: string; contentHash: string; contentType: string; bytes: number; }>` - Array of files to stage (base64-encoded content) | `Promise<PlayStagedFileRef[]>` |
| `mintStagedPlayFileUploads` | method | Mint short-lived presigned upload targets for staged play files.<br /><br />Internal primitive used by `stagePlayFiles`. The server returns an<br />already-staged ref (no upload needed) for content-addressed files it<br />already holds, or a presigned PUT URL the caller uploads the body to. | `files: Array<{ logicalPath: string; contentHash: string; contentType: string; bytes: number; }>` | `Promise<MintStagedFileUpload[]>` |
| `resolveStagedPlayFiles` | method | Resolve staged play files by content hash without uploading bytes.<br /><br />Missing files are returned so callers can upload only the files the server<br />does not already have. | `files: Array<{ logicalPath: string; contentHash: string; contentType: string; bytes: number; }>` | `Promise<{ files: PlayStagedFileRef[]; missing: Array<{ logicalPath: string; contentHash: string }>; }>` |
| `getPlayStatus` | method | Get the current status of a play execution.<br /><br />Internal/advanced primitive. Public callers should usually prefer<br />`runPlay`, `PlayJob.get`, or `deepline plays run --watch`. | `workflowId: string` - Play-run id from `startPlayRun`<br />`options?: { billing?: boolean; full?: boolean }` | `Promise<PlayStatus>` |
| `streamPlayRunEvents` | method | Stream semantic play-run events using the same SSE feed as the dashboard.<br /><br />The server emits a canonical `play.run.snapshot` event first for every<br />connection, then incremental live events until terminal state or reconnect. | `workflowId: string`<br />`options?: { signal?: AbortSignal; lastEventId?: string; mode?: 'cli' \| 'ui'; }` | `AsyncGenerator<PlayLiveEvent>` |
| `cancelPlay` | method | Cancel a running play execution.<br /><br />Sends a stop request for the run. | `workflowId: string` - Public Deepline play-run id to cancel | `Promise<void>` |
| `stopPlay` | method | Stop a running play execution, including open HITL waits. | `workflowId: string` - Public Deepline play-run id to stop<br />`options?: { reason?: string }` | `Promise<StopPlayRunResult>` |
| `listPlayRuns` | method | List recent runs for a named play.<br /><br />Returns runs sorted by start time (newest first), including workflow IDs,<br />status, timestamps, and metadata. | `playName: string` - The play name to query | `Promise<PlayRunListItem[]>` |
| `getRunStatus` | method | Get a run by id using the public runs resource model.<br /><br />This is the SDK equivalent of:<br /><br />```bash<br />deepline runs get <run-id> --json<br />``` | `runId: string`<br />`options?: RunsGetOptions` | `Promise<PlayStatus>` |
| `listRuns` | method | List play runs using the public runs resource model.<br /><br />This is the SDK equivalent of:<br /><br />```bash<br />deepline runs list --play <play-name> --status failed --json<br />``` | `options: RunsListOptions` | `Promise<PlayRunListItem[]>` |
| `observeRunEvents` | method | Observe one run's live events. Uses the Convex Run Snapshot subscription<br />transport first (ADR-0008), then falls back to the canonical SSE stream<br />when the subscription transport or its optional client modules are not<br />available. Pass `fallback: 'none'` to receive<br />`RunObserveTransportUnavailableError` instead. | `runId: string`<br />`options?: { signal?: AbortSignal; onNotice?: (message: string) => void; fallback?: 'sse' \| 'none'; }` | `AsyncGenerator<PlayLiveEvent>` |
| `tailRun` | method | Read the canonical run stream until a terminal run status is observed.<br /><br />Tries the Convex Run Snapshot subscription transport first (ADR-0008);<br />when the server cannot serve it (grant endpoint missing/unconfigured or<br />Convex unreachable) it falls back — with one `onNotice` message — to the<br />support-window SSE stream below.<br /><br />Server stream windows are finite: they end cleanly at the function<br />ceiling even while the run keeps executing. A window that ends (cleanly<br />or via transient network error) without a terminal event triggers one<br />durable-status re-check followed by a backed-off reconnect, so long runs<br />tail to completion. Abort via `options.signal` to stop waiting. | `runId: string`<br />`options?: RunsTailOptions` | `Promise<PlayStatus>` |
| `getRunInput` | method | Get the exact original input retained for a run. This is intentionally separate from status. | `runId: string` | `Promise<{ runId: string; input: Record<string, unknown> \| unknown[]; bytes: number; sha256: string \| null; replayedFromRunId: string \| null; }>` |
| `rerun` | method | Start a fresh run from a prior run's retained input and pinned revision. | `runId: string` | `Promise<{ runId: string; replayedFromRunId: string; revisionId: string \| null; status: string; next: { inspect: string; input: string }; }>` |
| `getRunLogs` | method | Fetch persisted logs for a run using the public runs resource model.<br /><br />This is the SDK equivalent of:<br /><br />```bash<br />deepline runs logs <run-id> --limit 200 --json<br />``` | `runId: string`<br />`options?: RunsLogsOptions` | `Promise<RunsLogsResult>` |
| `getPlaySheetRows` | method | Export persisted runtime-sheet rows for a play dataset/table namespace.<br /><br />This is the SDK form of exporting `ctx.dataset(...).run()` output for a<br />specific play and optional run id. | `input: { playName: string; tableNamespace: string; runId?: string; limit?: number; offset?: number; rowMode?: 'output' \| 'all'; }` | `Promise<PlaySheetRowsResult>` |
| `stopRun` | method | Stop a run by id using the public runs resource model.<br /><br />This is the SDK equivalent of:<br /><br />```bash<br />deepline runs stop <run-id> --reason "stale lock" --json<br />``` | `runId: string`<br />`options?: { reason?: string }` | `Promise<StopPlayRunResult>` |
| `stopAllRuns` | method | Stop every active run visible to the current workspace.<br /><br />This is the SDK equivalent of:<br /><br />```bash<br />deepline runs stop-all --reason "stale lock" --json<br />```<br /><br />Use this when a failed parent run left child or waiting runs active and you<br />need to clear the workspace run-slot state without knowing each run id. | `options?: { reason?: string; }` | `Promise<StopAllPlayRunsResult>` |
| `listPlays` | method | List callable plays visible to the workspace.<br /><br />Pass `origin: "prebuilt"` for Deepline-managed prebuilts or<br />`origin: "owned"` for org-owned plays. | `options?: { origin?: 'prebuilt' \| 'owned'; grep?: string; grepMode?: 'all' \| 'any' \| 'phrase'; categories?: string \| string[]; includeToolCategories?: boolean; includeArchived?: boolean; }` | `Promise<PlayListItem[]>` |
| `setPlayPinned` | method | Set whether an org-owned Play sorts before unpinned Plays. | `playName: string`<br />`pinned: boolean` | `Promise<{ name: string; pinned: boolean }>` |
| `getNotificationSettings` | method | Read product-notification destinations, subscriptions, event catalog, and DLQ health. | | `Promise<ProductNotificationSettings>` |
| `connectNotificationSlack` | method | Start the Slack OAuth flow required by product notifications. | `options?: { successUrl?: string; failureUrl?: string; }` | `Promise<{ ok: boolean; redirect_url: string }>` |
| `listNotificationSlackChannels` | method | List Slack channels visible to the connected Deepline Slack app. | `query?: string` | `Promise<{ identity: { teamId: string; teamName?: string }; channels: Array<{ id: string; name: string; isPrivate: boolean }>; }>` |
| `setNotificationSlack` | method | Select a Slack channel or direct member used for product notifications. | `destination: string \| { memberId: string }` | `Promise<unknown>` |
| `testNotificationSlack` | method | Send one synchronous test ping and return Slack's delivery result. | | `Promise<{ ok: boolean; deliveryId: string; state: string; message: string; }>` |
| `disableNotificationSlack` | method | Disable Slack product notifications without deleting the OAuth connection. | | `Promise<unknown>` |
| `setNotificationSubscriptions` | method | Enable or disable event IDs from the server-provided notification catalog. | `eventTypes: string[]`<br />`enabled: boolean` | `Promise<unknown>` |
| `listNotificationDlq` | method | List exhausted deliveries. Dead-lettered messages never replay automatically. | `limit?: number` | `Promise<unknown>` |
| `getNotificationDlqDelivery` | method | Inspect one exhausted notification delivery. | `deliveryId: string` | `Promise<unknown>` |
| `updateNotificationDlqDelivery` | method | Explicitly retry or archive one dead-lettered notification delivery. | `deliveryId: string`<br />`action: 'retry' \| 'archive'` | `Promise<unknown>` |
| `getNotifications` | method | List the workspace's named notification rules. | | `Promise<ProductNotificationSettings>` |
| `listNotificationChannels` | method | List Slack channels available to an already-connected Slack integration. | `query?: string` | `Promise<{ identity: { teamId: string; teamName?: string }; channels: Array<{ id: string; name: string; isPrivate: boolean }>; }>` |
| `createNotification` | method | Create a named notification routed through an existing provider integration. | `input: CreateNotificationInput` | `Promise<unknown>` |
| `updateNotification` | method | Update a notification's target, event selection, or enabled state. | `notificationId: string`<br />`input: UpdateNotificationInput` | `Promise<unknown>` |
| `testNotification` | method | Send a validation ping to one notification. | `notificationId: string` | `Promise<{ ok: boolean; deliveryId: string; state: string; message: string; }>` |
| `deleteNotification` | method | Archive one notification without touching its provider integration. | `notificationId: string` | `Promise<{ deleted: boolean; id: string }>` |
| `searchPlays` | method | Search callable plays and return compact play descriptions.<br /><br />Prebuilt plays are preferred by default because they have maintained<br />contracts and stable run behavior. | `options: { query: string; compact?: boolean; scope?: 'prebuilt' \| 'owned' \| 'all'; }` | `Promise<PlayDescription[]>` |
| `getPlay` | method | Get the full definition and state of a named play.<br /><br />Returns the play's revision state (draft, live), recent runs,<br />sheet processing summary, and database URL. | `name: string` - Play name<br />`options?: { source?: 'working' \| 'live' \| `version:${number}`; guidance?: boolean; }` | `Promise<PlayDetail>` |
| `describePlay` | method | Get a normalized play description suitable for agents and CLIs.<br /><br />The description includes runnable examples, input/output summaries, clone<br />guidance, revision state, and latest run metadata when available. | `name: string`<br />`options?: { compact?: boolean }` | `Promise<PlayDescription>` |
| `clearPlayHistory` | method | Clear run history and durable sheet/result data for a play without deleting<br />the play definition or revisions. | `name: string`<br />`request?: ClearPlayHistoryRequest` | `Promise<ClearPlayHistoryResult>` |
| `listPlayVersions` | method | List saved versions for a named play.<br /><br />Returns immutable revision snapshots newest-first, including the revision<br />id needed for exact-version runs and live-version switching. | `name: string` - Play name<br />`options?: { full?: boolean }` | `Promise<PlayRevisionSummary[]>` |
| `publishPlayVersion` | method | Make a play revision live.<br /><br />When `revisionId` is omitted, the current working revision becomes live.<br />The live version is what executes when the play is run by name without<br />specifying an explicit revision. | `name: string` - Play name<br />`request?: PublishPlayVersionRequest` - Optional explicit revision to make live | `Promise<PublishPlayVersionResult>` |
| `deletePlay` | method | Move an org-owned play to Trash. This disables its active triggers while<br />retaining its revisions and run history so it can be restored. Deepline<br />prebuilt plays are read-only. | `name: string` | `Promise<DeletePlayResult>` |
| `restorePlay` | method | Restore an org-owned play that was previously moved to Trash. | `name: string` | `Promise<RestorePlayResult>` |
| `getSharePage` | method | Current share status for a play: the public page (if any), the published<br />copy, and the revision picker. Read-only. | `name: string`<br />`options?: { revisionId?: string }` | `Promise<SharePageStatus>` |
| `publishSharePage` | method | Publish (or repoint) the play's public share page to a revision. Requires<br />`acknowledgedUnlisted: true` — the page is publicly viewable. Org-admin only. | `name: string`<br />`request: PublishSharePageRequest` | `Promise<SharePageStatus>` |
| `updateSharePage` | method | Update share-page settings (SEO indexing, credit-cost / latency display)<br />without moving the published pointer. Org-admin only. | `name: string`<br />`request: UpdateSharePageRequest` | `Promise<SharePageStatus>` |
| `unpublishSharePage` | method | Unshare: hard-delete the play's public page and its cards. Returns the<br />fresh status (now `share: null`). Org-admin only. Idempotent — a no-op when<br />the play was never published. | `name: string` | `Promise<SharePageStatus>` |
| `regenerateSharePage` | method | Regenerate the LLM landing-page copy for a revision (defaults to the<br />published one). Org-admin only. | `name: string`<br />`request?: { revisionId?: string }` | `Promise<SharePageStatus>` |
| `runPlay` | method | Run a play end-to-end: submit, stream until terminal, return result.<br /><br />This is the highest-level play execution method. It submits the play,<br />reads the canonical run stream for status updates, and returns a structured<br />result with logs and timing. Supports cancellation via `AbortSignal`. | `code: string` - Source string fallback; pass the bundled artifact in `options.artifact`<br />`csvPath: string \| null` - Input CSV path, or `null`<br />`name?: string` - Play name<br />`options?: { onProgress?: (status: PlayStatus) => void; signal?: AbortSignal; input?: Record<string, unknown>; sourceCode?: string; artifact?: Record<string, unknown>; compilerManifest?: PlayCompilerManifest; inputFile?: PlayStagedFileRef \| null; packagedFiles?: PlayStagedFileRef[]; force?: boolean; forceToolRefresh?: boolean; }` - Execution options | `Promise<PlayRunResult>` |
| `getBillingPlans` | method | Published plans plus the caller's active plan: prices, monthly grant<br />credits, rollover policy, and which plans are open for subscription.<br />Prefer `client.billing.plans()`. | | `Promise<BillingPlansResult>` |
| `topUpBillingBalance` | method | Charge the saved payment method and add Deepline credits to the active<br />workspace. Prefer `client.billing.topUp(...)`. | `options: { credits: number; idempotencyKey?: string; }` | `Promise<BillingTopUpResult>` |
| `getBillingSubscriptionStatus` | method | Subscription state for the active workspace: active plan, whether a<br />Stripe subscription backs it, renewal/cancellation facts, and remaining<br />Deepline credit pools. Prefer `client.billing.subscription.status()`. | | `Promise<BillingSubscriptionStatus>` |
| `cancelBillingSubscription` | method | Schedule subscription cancellation at period end, or reverse a pending<br />cancellation with `{ undo: true }`. The customer keeps the cycle they<br />paid for and every remaining credit — cancellation never claws back<br />credits. Prefer `client.billing.subscription.cancel(...)`. | `options?: { undo?: boolean; }` | `Promise<BillingSubscriptionCancelResult>` |
| `listBillingInvoices` | method | Customer-facing billing history: subscription invoices plus one-time<br />credit purchase receipts, newest first, with Stripe-hosted links.<br />Prefer `client.billing.invoices.list(...)`. | `options?: { limit?: number; }` | `Promise<BillingInvoicesResult>` |
| `getTargetBillingPlans` | method | List the reviewed target plans and whether new acquisition is enabled. | | `Promise<TargetBillingPlansResult>` |
| `getTargetBillingStatus` | method | Read the workspace's normalized target plan, payment, and balance state. | | `Promise<TargetBillingStatusResult>` |
| `getTargetAutoRecharge` | method | Read the canonical Metronome automatic recharge configuration. | | `Promise<TargetAutoRechargeResult>` |
| `updateTargetAutoRecharge` | method | Update automatic recharge and return the server-verified configuration. | `options: TargetAutoRechargeUpdateOptions` | `Promise<TargetAutoRechargeResult>` |
| `purchaseTargetBillingCredits` | method | Purchase target-billing credits through the durable commercial operation<br />flow. The caller supplies an idempotency key for safe retries. | `options: { credits: number; idempotencyKey: string; }` | `Promise<TargetBillingMutationResult>` |
| `transitionTargetBillingPlan` | method | Start, change, cancel, or restore a target plan through one idempotent<br />commercial operation. | `options: TargetBillingPlanTransitionOptions` | `Promise<TargetBillingMutationResult>` |
| `createTargetBillingPortalSession` | method | Create a Stripe-hosted portal session for payment recovery and invoices. | | `Promise<{ url: string }>` |
| `createWorkspace` | method | Create an additional workspace through the durable PAYG workflow. | `options: { name: string; idempotencyKey: string; }` | `Promise<WorkspaceCreateResult>` |
| `health` | method | Check API connectivity and server health. | | `Promise<{ status: string; version?: string; status_banner?: { message: string; updatedAt: number; }; }>` |
### `client.runs`
Public runs namespace exposed as `client.runs`.
This namespace mirrors the canonical `/api/v2/runs` resource family and is
the preferred low-level surface for polling, streaming, stopping, reading
logs, and exporting durable dataset rows.
#### Fields
<!-- 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) => Promise<{ runId: string; replayedFromRunId: string; revisionId: string \| null; status: string; next: { inspect: string; input: string }; }>` | 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. |
| `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?: { reason?: string }) => Promise<StopAllPlayRunsResult>` | Yes | Stop active runs across the current workspace. |
### `client.billing`
Public billing namespace exposed as `client.billing`.
Carries plans, subscription state, cancellation, and invoice/receipt history
so CLI commands and programmatic callers share one surface.
#### Fields
<!-- prettier-ignore -->
| Name | Type | Required | Description |
|---|---|---:|---|
| `topUp` | `(options: { credits: number; idempotencyKey?: string; }) => Promise<BillingTopUpResult>` | Yes | Charge the saved payment method and add Deepline credits to the active workspace. |
| `plans` | `() => Promise<BillingPlansResult>` | Yes | Published plans plus the plan you are on ("what plans exist and what am I on"). |
| `subscription` | `{ status: () => Promise<BillingSubscriptionStatus>; cancel: (options?: { undo?: boolean; }) => Promise<BillingSubscriptionCancelResult>; }` | Yes | |
| `invoices` | `{ list: (options?: { limit?: number }) => Promise<BillingInvoicesResult>; }` | Yes | |
| `targetPlans` | `() => Promise<TargetBillingPlansResult>` | Yes | Metronome-authored target catalog and current Contract projection. |
| `targetStatus` | `() => Promise<TargetBillingStatusResult>` | Yes | Normalized target billing state. |
| `autoRecharge` | `{ get: () => Promise<TargetAutoRechargeResult>; update: ( options: TargetAutoRechargeUpdateOptions, ) => Promise<TargetAutoRechargeResult>; }` | Yes | Read and manage the Metronome-backed automatic recharge configuration. |
| `purchaseCredits` | `(options: { credits: number; idempotencyKey: string; }) => Promise<TargetBillingMutationResult>` | Yes | Buy Deepline credits through a payment-gated Metronome commit. |
| `transitionPlan` | `( options: TargetBillingPlanTransitionOptions, ) => Promise<TargetBillingMutationResult>` | Yes | Start, change, cancel, or undo a target plan transition. |
| `portalSession` | `() => Promise<{ url: string }>` | Yes | Create a Stripe-hosted billing Portal session. |
### `client.monitors`
Public monitors namespace exposed as `client.monitors`.
Mirrors the /api/v2/monitors resource family so the monitors CLI and
programmatic callers share one product surface — every `deepline monitors`
verb maps to a method here. Monitors are fully expressible as SDK code: author
a definition with `defineMonitor`, then check/deploy/list/get/update/
delete/reactivate through this namespace.
#### Fields
<!-- prettier-ignore -->
| Name | Type | Required | Description |
|---|---|---:|---|
| `status` | `() => Promise<MonitorsAccessStatus>` | Yes | Whether the current workspace can use monitors (`{ has_access, reason }`). |
| `available` | `( toolIdOrOptions?: string \| (MonitorsAvailableOptions & { tool?: string }), options?: MonitorsAvailableOptions, ) => Promise<MonitorsAvailableResult>` | Yes | The deployable monitor tools catalog. Call with no tool id for the list, or<br />with a tool id (positional or `{ tool }`) to describe one tool's full<br />payload/stream contract. |
| `check` | `(definition: MonitorDefinition) => Promise<MonitorCheckResult>` | Yes | Validate a monitor definition without deploying it (no spend). |
| `deploy` | `( definition: MonitorDefinition, options?: { dryRun?: boolean }, ) => Promise<MonitorDeployResult>` | Yes | Deploy a monitor from a definition. May spend Deepline credits. |
| `list` | `(options?: MonitorsListOptions) => Promise<MonitorsListResult>` | Yes | List deployed monitors (active by default). `includeConsumers` requires a limit of 20 or fewer. |
| `get` | `(key: string) => Promise<MonitorDetail>` | Yes | Fetch one deployed monitor by public key with bounded current listener health. |
| `test` | `( key: string, payload: Record<string, unknown>, options?: MonitorTestOptions, ) => Promise<MonitorTestResult>` | Yes | Test a deployed monitor's callback envelope without side effects. |
| `validate` | `(key: string) => Promise<MonitorValidateResult>` | Yes | |
| `dependents` | `(key: string) => Promise<MonitorDependents>` | Yes | List the published plays depending on one monitor's output streams. |
| `update` | `( key: string, patch: Record<string, unknown>, ) => Promise<MonitorUpdateResult>` | Yes | Update a deployed monitor by public key. |
| `delete` | `( key: string, options?: { dryRun?: boolean }, ) => Promise<MonitorDeleteResult>` | Yes | Delete a deployed monitor and its upstream provider resource. `dryRun` returns the delete plan. |
| `reactivate` | `( key: string, options?: { dryRun?: boolean }, ) => Promise<MonitorReactivateResult>` | Yes | Reactivate a disabled monitor. `dryRun` returns the reactivation cost. |
| `audit` | `(options?: { fleetId?: string; cursor?: string \| null; }) => Promise<MonitorsAuditResult>` | Yes | Re-read what the provider holds onto the monitors that claim it. Bounded<br />and idempotent: pass `cursor` back while `audit.cursor` is non-null. |
| `repair` | `(options?: { fleetId?: string; dryRun?: boolean; }) => Promise<MonitorsRepairResult>` | Yes | Converge the monitors whose desired and observed states disagree.<br />`dryRun` returns the same plan without queueing anything. |
| `health` | `(options?: { fleetId?: string }) => Promise<MonitorsHealth>` | Yes | Delivery and convergence health for the workspace or one fleet. |
| `fleets` | `MonitorFleetsNamespace` | Yes | Define, reconcile, and control table-backed monitor fleets. |
scripts/clay-extract-bookmarklet.js
/*
* 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)');
}
})();
scripts/clay-extract-bookmarklet.url.txt
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})();scripts/clay-extract.py
#!/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()
scripts/clay-har-miner.py
#!/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']}]")
scripts/contact-accuracy-audit.py
#!/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()
scripts/fixtures_contact_accuracy_audit.json
[
{
"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"]}
]
}
]
scripts/fixtures_current_role.json
[
{
"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"}
}
]
scripts/fixtures_name_validation.json
[
{
"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
}
]
scripts/flatten-search-contact-persons.py
#!/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()
scripts/select-current-role.py
#!/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()
scripts/validate-emails.py
#!/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()
scripts/validate-linkedin-names.py
#!/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()
skill-metadata.json
{
"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
---
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,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,hackernews,harvestapi,heyreach,hubspot,hunter,icypeas,instantly,intercom,ipqs,kernel,leadmagic,lemlist,limadata,linkedin_ads_audiences,linkedin_scraper,lusha,meta_audiences,nooks,openmart,opensosdata,openwebninja,outreach,parallel,peopledatalabs,podscan,postgres,predictleads,prospeo,quickenrich,redshift,rocketreach,salesforce,salesforge,salesloft,scrapecreators,searchbug,sec_edgar,sentrion,serper,slack,smartlead,snowflake,sumble,theirstack,trestle,…."
---
# 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 full `deepline monitors` command set (status, available, check, deploy, list, get, update, delete, reactivate), monitor definition shape, the provider-webhook → Customer DB → triggered-play data flow, and the 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-pre-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), [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_ip_to_company](provider-playbooks/deepline_ip_to_company.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), [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), [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), [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), [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), [sumble](provider-playbooks/sumble.md), [theirstack](provider-playbooks/theirstack.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.
- Estimate play pricing before full scale: `deepline plays list --show-cost`, the play's `describe` output, and the pilot's observed cost from `deepline runs get <run-id> --full --json`. State the estimate in the approval message. `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_person_enrichment`, `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)
Do not wait for the user to ask. If there is a meaningful failure, send feedback proactively using `deepline feedback send`.
Trigger when any of these happen:
- A provider/tool call fails repeatedly.
- Output is clearly wrong for the requested task.
- A CLI/runtime bug blocks completion.
- You needed a significant workaround to finish.
Run once per issue cluster (avoid spam), and include:
- workflow goal
- tool/provider/model used
- failure point and exact error details
- reproduction steps attempted
```bash
deepline feedback send "Goal: <goal>. Tool/provider/model: <details>. Failure: <what broke>. Error: <exact message>. Repro attempted: <steps>."
```
### 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
```
- **No** -> do not send the session.
Ask once per completed run. Do not nag or re-ask unless the user starts a new run/session.writing-outreach.md
# 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.