> ## Documentation Index
> Fetch the complete documentation index at: https://deepline.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Batch CSV Enrichment

> Turn a spreadsheet of leads or accounts into a retryable Deepline play. Use it for a first run, result review, and safe scaling.

Job: enrich a list without losing track of which row worked, failed, or needs review.

## Recommended CSV shape

Use a stable row key.

```csv theme={null}
lead_id,first_name,last_name,linkedin_url,company_name,company_domain
lead-001,Jane,Smith,https://www.linkedin.com/in/example-person/,ExampleCo,example.com
```

Here is the starter shape for this workflow.

If you are reading this outside the repo, create the same starter file:

```bash theme={null}
cat > leads.csv <<'CSV'
lead_id,first_name,last_name,linkedin_url,company_name,company_domain
lead-001,Jane,Smith,https://www.linkedin.com/in/example-person/,ExampleCo,example.com
lead-002,Sam,Lee,https://www.linkedin.com/in/example-two/,Acme,acme.com
CSV
```

| Column                           | Required             | Why                                             |
| -------------------------------- | -------------------- | ----------------------------------------------- |
| `lead_id`                        | Yes                  | Stable row key for retries and output review    |
| `linkedin_url`                   | Yes for this example | Best starting point for the prebuilt email play |
| `first_name`, `last_name`        | Helpful              | Improves matching                               |
| `company_name`, `company_domain` | Helpful              | Improves matching and validation                |

If your file has different column names, ask Claude Code to map your columns to this shape before running.

## The play pattern

The workflow maps each row to an email step.

```ts theme={null}
import { definePlay } from 'deepline';

type Lead = {
  lead_id: string;
  first_name: string;
  last_name: string;
  linkedin_url: string;
  company_name: string;
  company_domain: string;
};

export default definePlay('lead-email-waterfall', async (ctx, input: { csv: string }) => {
const leads = await ctx.csv<Lead>(input.csv);

const withEmail = await ctx
  .dataset('lead_email', leads)
  .withColumn('email', async (lead, rowCtx) => {
    const result = await rowCtx.tools.execute({
      id: 'person_email',
      tool: 'test_rate_limit',
      input: {
        key: lead.linkedin_url,
      },
      description: 'Find a verified work email for this lead.',
    });
    return result.status === 'SUCCESS'
      ? `${lead.first_name}.${lead.last_name}@${lead.company_domain}`.toLowerCase()
      : null;
  })
  .run({ key: 'lead_id', description: 'Find one verified work email per lead.' });

  const rows = await ctx
    .dataset('email_validation', withEmail)
    .withColumn('email_status', async (lead, rowCtx) => {
      if (!lead.email) return 'missing';
      const result = await rowCtx.tools.execute({
        id: 'verify_email',
        tool: 'test_rate_limit',
        input: { key: String(lead.email) },
        description: 'Validate the candidate email.',
      });
      return result.status;
    })
    .run({ key: 'lead_id', description: 'Validate the emails that were found.' });

  return { rows };
});
```

## Pilot it first

The play owns its CSV contract. A play that declares `input.csv` can be run with `--csv`. Reserved run flags such as `--file` keep their command meaning, so a play that declares `input.file` should be run with `--input '{"file":"leads.csv"}'`. This example reads rows with `ctx.csv(input.csv)`.

```bash theme={null}
deepline plays check docs-examples/sdk-v2/lead-email-waterfall.play.ts

head -n 4 leads.csv > /tmp/leads-pilot.csv

deepline plays run docs-examples/sdk-v2/lead-email-waterfall.play.ts \
  --csv /tmp/leads-pilot.csv \
  --watch

deepline runs export <run-id> --out /tmp/leads-pilot-enriched.csv
```

Inspect `/tmp/leads-pilot-enriched.csv`. You want the original columns plus the email and validation columns added by the play.

## Keep bulk data in files

Play input is for control parameters and file references. Do not pass a full
spreadsheet as inline JSON through `--input`.

Use `ctx.csv(input.csv)` with a staged file path instead:

```bash theme={null}
deepline plays run docs-examples/sdk-v2/lead-email-waterfall.play.ts \
  --csv leads.csv \
  --watch
```

Inline submitted JSON has a hard 1 MiB ceiling. Payloads above that are
rejected with guidance to use staged files or `ctx.csv` inputs. Deepline
automatically externalizes moderately large retry payloads between 100 KB and
1 MiB so runs can recover from platform retries, but row data should still live
in CSV files.

## Run the full file

```bash theme={null}
deepline plays run docs-examples/sdk-v2/lead-email-waterfall.play.ts \
  --csv leads.csv \
  --watch

deepline runs export <run-id> --out leads-enriched.csv
```

## Why this matters

`ctx.dataset` makes every row resumable. If row 83 fails, you do not need to rerun the whole list blindly.

If a row is missing required input, keep it in the output and mark it for review instead of deleting it silently.

For scheduled refreshes, add a stale window:

```ts theme={null}
import { definePlay } from 'deepline';

type Account = {
  domain: string;
};

export default definePlay('account-refresh', async (ctx, input: { accounts: Account[] }) => {
  await ctx
    .dataset('account_refresh', input.accounts)
    .withColumn('company', (account, rowCtx) =>
      rowCtx.tools.execute({
        id: 'company',
        tool: 'test_rate_limit',
        input: { key: account.domain },
        description: 'Refresh the company profile.',
        staleAfterSeconds: 86_400,
      }),
    )
    .run({ key: 'domain' });

  return { ok: true };
});
```

That says: reuse today's completed rows, refresh tomorrow.
