Metadata-Version: 2.4
Name: autotouch-cli
Version: 0.2.32
Summary: Autotouch Smart Table CLI
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0
Requires-Dist: python-dotenv>=1.0.0

# Autotouch CLI Reference (`autotouch`)

This page documents the installable CLI for the Smart Table developer API.
It is the canonical CLI doc in this repo and the package readme shipped to PyPI.
Use it when you want less boilerplate than raw HTTP/cURL while keeping the same API behavior, auth, and scopes.

## Start here

Use this order when you are orienting in the CLI:

1. Configure auth and confirm scopes with `autotouch auth check` and `autotouch capabilities`.
2. Use the API endpoint -> CLI command map when translating an existing API workflow.
3. Use `autotouch columns recipe` before creating provider-backed workflow columns.
4. Use `autotouch jobs get` as the source of truth for async run state.
5. Use `autotouch sequences ...` for sequence definitions/enrollments and `autotouch tasks ...` for task queue work.

### Quick decision guide

| If you want to... | Start here |
| --- | --- |
| create a table or inspect available tables | `autotouch tables create` / `autotouch tables list` |
| import CSV data safely | `autotouch rows import-csv --validate-only`, then `autotouch rows import-csv --wait` |
| create a provider-backed column | `autotouch columns recipe --type <TYPE>`, then `autotouch columns create` |
| run exactly `N` rows | `autotouch columns run-next` |
| stage a gradual rollout | `autotouch columns run --scope firstN --unprocessed-only` |
| run only the current filtered segment | `autotouch columns run --scope filtered --filters-file ...` |
| run one exact row or an explicit list of row IDs | `scope=row` for one ID, `scope=subset` for many IDs |
| verify whether a job is really done | `autotouch jobs get --job-id <JOB_ID>` |
| create projections from JSON output | `autotouch columns projections` |
| create/manage sequence definitions directly | `autotouch sequences recipe`, then `autotouch sequences create/update/activate` |
| enroll leads directly into a sequence | `autotouch sequences enroll --sequence-id <SEQUENCE_ID> ...` |
| enroll table rows into an existing sequence | `autotouch columns recipe --type add_to_sequence`, then `autotouch columns create` + `autotouch columns run` |
| seed org context from a company website | `autotouch onboarding submit-domain --website acme.com` |
| inspect or update org/personal value prop context | `autotouch org-context get/set`, `autotouch personal-context get/set`, `autotouch context resolved` |
| create or update a CRM lead | `autotouch leads recipe --type create`, then `autotouch leads create` / `autotouch leads update` |
| add email or phone contact info to a lead | `autotouch leads recipe --type contact_upsert`, then `autotouch leads upsert-contact-channels --lead-id <LEAD_ID> ...` |
| bulk update lead status or owner | `autotouch leads update-status` / `autotouch leads reassign-owner` |
| manage tasks directly | `autotouch tasks query`, `autotouch tasks get`, `autotouch tasks update`, `autotouch tasks draft` |
| query/filter CRM leads and attached research | `autotouch leads query` then `autotouch leads research` |

### Operating model

- This file is the full CLI reference and the package readme published to PyPI.
- The installed package gives you CLI entrypoints and package metadata; do not assume there is a separate installed docs directory.
- Research-table APIs, sequence commands, lead create/query/update operations, and public task-queue workflows are first-class CLI surface areas today.
- For async operations, backend bulk-job state is authoritative; local terminal output is only a convenience layer.
- For staged or cost-sensitive runs, estimate first and prefer filtered scopes plus `firstN` or `run-next`.

## Install

```bash
pipx install autotouch-cli
# or
pip install autotouch-cli
```

## Configure auth

Developer keys and scopes are identical to raw API usage (`stk_...`, same scope checks).

```bash
autotouch auth set-key --api-key stk_... --base-url https://app.autotouch.ai
autotouch auth check
autotouch auth show
autotouch auth whoami
```

For user/admin endpoints like onboarding, org context, and personal context, sign in with a user session too:

```bash
autotouch auth login --email ada@yourcompany.com
```

Credentials are stored in `~/.config/autotouch/config.json` by default.
Override path with `AUTOTOUCH_CONFIG_PATH`.

Developer key scope reference (including workflow scopes like `sequences:*` and `tasks:*`):
- `docs/platform/authentication.md`

## Agent bootstrap (one-call signup + key)

If an agent/user does not have an account + developer key yet, bootstrap both in one call:

```bash
autotouch auth bootstrap \
  --first-name Ada \
  --last-name Lovelace \
  --email ada@yourcompany.com \
  --password 'use-a-strong-random-password' \
  --organization-name "Your Company" \
  --save-key
```

Or pass a full payload file:

```bash
autotouch auth bootstrap --data-file bootstrap.json --save-key
```

Notes:
- New orgs created through signup/bootstrap start with `50` credits.
- `--save-key` stores the returned `apiKey` and also signs in the same user so the saved config has both the developer key and the user session.
- Identity linking is email-based: later human sign-in with the same normalized email maps to the same user/org.
- `auth whoami` shows which user/org the saved session belongs to.

## Onboarding and context

Recommended setup for agent-mode LLM enrichment:

```bash
autotouch auth login --email ada@yourcompany.com
autotouch onboarding submit-domain --website yourcompany.com
autotouch org-context get
autotouch personal-context get
autotouch context resolved
```

Notes:
- `submit-domain` seeds organization context from the company website.
- `org-context set` is where you refine value proposition, ideal titles, buyer persona, user persona, and voice profile.
- `personal-context set` is where you refine rep-specific title, territories, personal value proposition, and personal voice profile.
- `context resolved` shows the effective requester context the enrichment/runtime layer will actually use after org + personal overrides.
- Agent-mode prompt generation and compiled LLM enrichment prompts always include requester/company context.
- Basic mode stays manual-prompt-first; write the prompt directly based on the user goal/preferences and include explicit placeholders when row data should be referenced.

## API endpoint -> CLI command map

| API endpoint | CLI command |
| --- | --- |
| `GET /api/capabilities` | `autotouch capabilities` |
| `POST /api/tables` | `autotouch tables create --name "My Table"` |
| `GET /api/tables?view_mode=org` | `autotouch tables list --view-mode org` |
| `POST /api/tables/{table_id}/rows` | `autotouch rows add --table-id <TABLE_ID> --records-file rows.json` |
| `POST /api/tables/{table_id}/import-optimized` | `autotouch rows import-csv --table-id <TABLE_ID> --file contacts.csv` |
| `POST /api/tables/{table_id}/csv-validate` | `autotouch rows import-csv --table-id <TABLE_ID> --file contacts.csv --validate-only` |
| `GET /api/tables/{table_id}/import-status/{task_id}` | `autotouch rows import-status --table-id <TABLE_ID> --task-id <TASK_ID>` |
| `GET /api/tables/{table_id}/import-verify/{task_id}` | `autotouch rows import-verify --table-id <TABLE_ID> --task-id <TASK_ID> --expected-rows <N>` |
| `POST /api/tables/{table_id}/import-rollback/{task_id}` | `autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID>` |
| `GET /api/blacklist` | `autotouch blacklist list --type-filter all --limit 100` |
| `POST /api/blacklist` | `autotouch blacklist add --type domain --value competitor.com` |
| `DELETE /api/blacklist/{entry_id}` | `autotouch blacklist remove --entry-id <ENTRY_ID>` |
| `POST /api/blacklist/import` | `autotouch blacklist import --file blacklist.csv` |
| `POST /api/blacklist/check` | `autotouch blacklist check --emails-file recipients.csv --emails-column email` |
| `POST /api/blacklist/filter` | `autotouch blacklist filter --emails-file recipients.csv --emails-column email` |
| `PATCH /api/tables/{table_id}/cells` | `autotouch cells patch --table-id <TABLE_ID> --updates-file updates.json` |
| `GET /api/tables/{table_id}/columns` | `autotouch columns list --table-id <TABLE_ID>` |
| `POST /api/tables/{table_id}/columns` | `autotouch columns create --table-id <TABLE_ID> --data-file column.json` |
| `PATCH /api/tables/{table_id}/columns/{column_id}` | `autotouch columns update --table-id <TABLE_ID> --column-id <COLUMN_ID> --data-file column-update.json` |
| `DELETE /api/tables/{table_id}/columns/{column_id}` | `autotouch columns delete --table-id <TABLE_ID> --column-id <COLUMN_ID> --yes` |
| `POST /api/tables/{table_id}/columns/projections` | `autotouch columns projections --table-id <TABLE_ID> --data-file projections.json` |
| `POST /api/tables/{table_id}/columns/{column_id}/estimate` | `autotouch columns estimate --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope all` |
| `POST /api/tables/{table_id}/columns/{column_id}/run` | `autotouch columns run --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope all` |
| `POST /api/tables/{table_id}/columns/{column_id}/stop` | `autotouch columns stop --table-id <TABLE_ID> --column-id <COLUMN_ID>` |
| `GET /api/bulk-jobs` | `autotouch jobs list --table-id <TABLE_ID> --column-id <COLUMN_ID> --limit 10` |
| `GET /api/bulk-jobs/{job_id}` | `autotouch jobs get --job-id <JOB_ID>` |
| `GET /api/sequences` | `autotouch sequences list --limit 50` |
| `GET /api/sequences/{sequence_id}` | `autotouch sequences get --sequence-id <SEQUENCE_ID>` |
| `GET /api/sequences/{sequence_id}/stats` | `autotouch sequences stats --sequence-id <SEQUENCE_ID>` |
| `GET /api/sequences/delivery-status` | `autotouch sequences delivery-status` |
| `POST /api/sequences` | `autotouch sequences create --data-file sequence.json` |
| `PUT /api/sequences/{sequence_id}` | `autotouch sequences update --sequence-id <SEQUENCE_ID> --data-file sequence.json` |
| `DELETE /api/sequences/{sequence_id}` | `autotouch sequences delete --sequence-id <SEQUENCE_ID> --yes` |
| `POST /api/sequences/{sequence_id}/clone` | `autotouch sequences clone --sequence-id <SEQUENCE_ID>` |
| `PATCH /api/sequences/{sequence_id}/status` | `autotouch sequences status --sequence-id <SEQUENCE_ID> --status ACTIVE` |
| `POST /api/sequences/{sequence_id}/enroll` | `autotouch sequences enroll --sequence-id <SEQUENCE_ID> --lead-id <LEAD_ID>` |
| `POST /api/sequences/{sequence_id}/enrollments/pause` | `autotouch sequences pause-enrollment --sequence-id <SEQUENCE_ID> --lead-id <LEAD_ID>` |
| `GET /api/leads/{lead_id}` | `autotouch leads get --lead-id <LEAD_ID>` |
| `POST /api/leads` | `autotouch leads create --data-file lead.json` |
| `PUT /api/leads/{lead_id}` | `autotouch leads update --lead-id <LEAD_ID> --data-file patch.json` |
| `POST /api/leads/{lead_id}/contact-channels` | `autotouch leads upsert-contact-channels --lead-id <LEAD_ID> --data-file contacts.json` |
| `POST /api/leads/bulk_update_status` | `autotouch leads update-status --lead-id <LEAD_ID> --status contacting` |
| `POST /api/leads/bulk_reassign_owner` | `autotouch leads reassign-owner --lead-id <LEAD_ID> --user-id <USER_ID>` |
| `POST /api/leads/query` | `autotouch leads query --filter-file lead-filter.json --limit 50` |
| `POST /api/leads/query/count` | `autotouch leads count --filter-file lead-filter.json` |
| `POST /api/leads/query/stats` | `autotouch leads stats --filter-file lead-filter.json` |
| `POST /api/leads/query/research` | `autotouch leads research --lead-id <LEAD_ID> --source-table-id <TABLE_ID>` |
| `GET /api/leads/filters/metadata` | `autotouch leads filters-metadata` |
| `GET /api/leads/research-tables/available` | `autotouch leads research-tables` |
| `GET /api/tables/{table_id}/webhook` | `autotouch webhooks get --table-id <TABLE_ID>` |
| `POST /api/tables/{table_id}/webhook` | `autotouch webhooks rotate --table-id <TABLE_ID>` |
| `POST /api/webhooks/tables/{table_id}/ingest` | `autotouch webhooks ingest --table-id <TABLE_ID> --records-file records.json --webhook-token <WEBHOOK_TOKEN>` |
| `GET /api/webhook-subscriptions` | `autotouch webhooks subscriptions list --limit 100` |
| `POST /api/webhook-subscriptions` | `autotouch webhooks subscriptions create --data-file subscription.json` |
| `GET /api/webhook-subscriptions/{subscription_id}` | `autotouch webhooks subscriptions get --subscription-id <SUBSCRIPTION_ID>` |
| `PATCH /api/webhook-subscriptions/{subscription_id}` | `autotouch webhooks subscriptions update --subscription-id <SUBSCRIPTION_ID> --data-file patch.json` |
| `DELETE /api/webhook-subscriptions/{subscription_id}` | `autotouch webhooks subscriptions delete --subscription-id <SUBSCRIPTION_ID>` |
| `POST /api/webhook-subscriptions/{subscription_id}/pause` | `autotouch webhooks subscriptions pause --subscription-id <SUBSCRIPTION_ID>` |
| `POST /api/webhook-subscriptions/{subscription_id}/resume` | `autotouch webhooks subscriptions resume --subscription-id <SUBSCRIPTION_ID>` |
| `POST /api/webhook-subscriptions/{subscription_id}/rotate-secret` | `autotouch webhooks subscriptions rotate-secret --subscription-id <SUBSCRIPTION_ID>` |
| `POST /api/webhook-subscriptions/{subscription_id}/test` | `autotouch webhooks subscriptions test --subscription-id <SUBSCRIPTION_ID>` |
| `POST /api/auth/agent-bootstrap` | `autotouch auth bootstrap --data-file bootstrap.json --save-key` |
| `POST /api/auth/login` | `autotouch auth login --email ada@yourcompany.com` |
| `POST /api/onboarding/submit-domain` | `autotouch onboarding submit-domain --website yourcompany.com` |
| `GET /api/org/context` | `autotouch org-context get` |
| `POST /api/org/context` | `autotouch org-context set --data-file org-context.json` |
| `GET /api/onboarding/personal-context` | `autotouch personal-context get` |
| `POST /api/onboarding/personal-context` | `autotouch personal-context set --data-file personal-context.json` |
| `GET /api/llm/company-context` | `autotouch context resolved` |
| `POST /api/task-queue/query` | `autotouch tasks query --data-file task-query.json` |
| `POST /api/task-queue/query/count` | `autotouch tasks count --data-file task-query.json` |
| `POST /api/task-queue/assignee-counts` | `autotouch tasks assignee-counts --data-file task-query.json` |
| `GET /api/task-queue/{task_id}` | `autotouch tasks get --task-id <TASK_ID>` |
| `GET /api/task-queue/stats/summary` | `autotouch tasks stats` |
| `POST /api/task-queue/create` | `autotouch tasks create --data-file task-create.json --actor-user-id <USER_ID>` |
| `PUT /api/task-queue/{task_id}` | `autotouch tasks update --task-id <TASK_ID> --data-file task-update.json --actor-user-id <USER_ID>` |
| `DELETE /api/task-queue/{task_id}` | `autotouch tasks delete --task-id <TASK_ID> --actor-user-id <USER_ID>` |
| `POST /api/task-queue/bulk-update` | `autotouch tasks bulk-update --data-file task-bulk-update.json --actor-user-id <USER_ID>` |
| `POST /api/task-queue/bulk-delete` | `autotouch tasks bulk-delete --data-file task-bulk-delete.json --actor-user-id <USER_ID>` |
| `POST /api/task-queue/{task_id}/draft` | `autotouch tasks draft --task-id <TASK_ID> --force --actor-user-id <USER_ID>` |
| `POST /api/task-queue/{task_id}/email/schedule` | `autotouch tasks schedule-email --task-id <TASK_ID> --data-file schedule.json --actor-user-id <USER_ID>` |

## Delete column

Delete is a hard delete:
- removes the column definition
- removes all cells stored under that column
- requires `columns:write`

CLI:

```bash
autotouch columns delete --table-id <TABLE_ID> --column-id <COLUMN_ID> --yes
```

## Sequence commands

The CLI now exposes the public Sequences endpoints under `autotouch sequences`.

Common commands:
- `autotouch sequences recipe --type create --out-file sequence.json`
- `autotouch sequences recipe --type bulk_automated --out-file sequence.json`
- `autotouch sequences create --data-file sequence.json`
- `autotouch sequences update --sequence-id <SEQUENCE_ID> --data-file sequence.json`
- `autotouch sequences activate --sequence-id <SEQUENCE_ID>`
- `autotouch sequences pause --sequence-id <SEQUENCE_ID>`
- `autotouch sequences archive --sequence-id <SEQUENCE_ID>`
- `autotouch sequences enroll --sequence-id <SEQUENCE_ID> --lead-id <LEAD_ID> --wait`
- `autotouch sequences pause-enrollment --sequence-id <SEQUENCE_ID> --lead-id <LEAD_ID>`
- `autotouch sequences delivery-status`
- `autotouch sequences list --include-stats`

Important contract notes:
- Create/update via developer key still requires `sourceTableId`.
- Mutating sequence commands accept `--actor-user-id` for explicit actor resolution.
- Recommended default: personal delivery plus `EMAIL` steps with `emailSendMode=MANUAL` and `aiDraft=true`.
- Use `bulk_automated` only when the provider should send email automatically at scale. `aiDraft` does not apply to bulk automated email steps.
- Personal delivery uses the connected inbox for the enrolled owner (`assignedToUserId`, else the enrolling actor); there is no per-sequence personal sender picker yet.
- If that user has multiple connected personal inboxes for the same provider, the most recently updated one is used. Gmail is tried before Microsoft. Sender stays pinned after the first personal email in that enrollment.
- Bulk delivery uses `emailDelivery.bulkProvider`; Instantly can also narrow sending accounts via `emailDelivery.providerConfig.instantly.selectedAccounts`.
- `activate`, `pause`, and `archive` are convenience wrappers around `autotouch sequences status`.
- `autotouch sequences enroll --wait` polls `/api/bulk-jobs/{job_id}` and preserves the API response `task_id` as the bulk job id.
- Research-table `add_to_sequence` is still available through `autotouch columns create/update/run` and still targets an existing sequence.
- Real enrollment requires the target sequence to be `ACTIVE` unless direct enroll uses `reviewOnly=true`.

AI draft outputs:
- Manual new email: subject + body
- Manual reply email: reply body
- LinkedIn connect: connection note
- LinkedIn message: message copy
- Call: call script

Example:

```bash
autotouch sequences recipe --type create --out-file sequence.json
autotouch sequences create --data-file sequence.json
autotouch sequences activate --sequence-id <SEQUENCE_ID>
```

Cookbooks:

1. Recommended personal AI-assisted outreach

```bash
autotouch sequences recipe --type create --out-file sequence.json
autotouch sequences create --data-file sequence.json
autotouch sequences activate --sequence-id <SEQUENCE_ID>
autotouch sequences enroll --sequence-id <SEQUENCE_ID> --lead-ids-file lead_ids.json --wait
```

Notes:
- `recipe --type create` is the recommended default.
- It starts with personal delivery, a manual AI-drafted email, then AI-drafted call and LinkedIn follow-up steps.
- Favorite/starred research fields are the default grounding context when explicit `fieldIds` are not provided.

2. Bulk automated email sequence

```bash
autotouch sequences recipe --type bulk_automated --out-file sequence.json
autotouch sequences create --data-file sequence.json
autotouch sequences activate --sequence-id <SEQUENCE_ID>
autotouch sequences enroll --sequence-id <SEQUENCE_ID> --lead-ids-file lead_ids.json --wait
```

Notes:
- Use this when Instantly or Smartlead should send email automatically.
- Bulk automated email steps do not support `aiDraft`; author the templates directly in `subjectTemplate` and `bodyTemplate`.

Reference contract (actor model + manual/automated/AI-draft nuances):
- `docs/platform/external-workflows-api.md`

Signature controls for sequence payloads:
- `defaultAppendSignature` (optional, sequence-level) sets the default signature-append behavior for email steps.
- `steps[].appendSignature` (optional, step-level) overrides signature behavior for a specific email step.
- Signature append is deterministic at send time when enabled for the step.

## Leads API coverage

The CLI now exposes the public leads read + write endpoints under `autotouch leads`.

Use this pattern:

1. `autotouch leads create` when you need to create one lead from canonical JSON input.
2. `autotouch leads recipe` when you want a ready-to-edit payload for create, update, or contact upsert.
3. `autotouch leads update` for scalar or partial lead updates.
4. `autotouch leads upsert-contact-channels` when you need to add or merge emails/phones without replacing the full arrays.
5. `autotouch leads update-status` / `autotouch leads reassign-owner` for operational bulk changes by explicit IDs or descriptor selection.
6. `autotouch leads query` to page through leads, apply descriptor filters, and inspect `related_tables` / `research_summary`.
7. `autotouch leads research` when you need actual research field values for specific lead IDs from one source table.

Important contract notes:
- External callers should use `POST /api/leads/query` rather than the older `GET /api/leads` list route.
- `autotouch leads get` is the direct single-lead lookup path.
- Use `autotouch leads update` for scalar lead fields only.
- `autotouch leads update` rejects email/phone fields and points callers to `autotouch leads upsert-contact-channels`.
- Contact upsert merges into canonical `email_addresses` and `phone_numbers` instead of replacing those arrays.
- `update-status` and `reassign-owner` accept either explicit lead IDs or descriptor selection input (`--filter-json`, `--search`, `--scope`, etc.).
- `autotouch leads query` accepts the same descriptor payload shape as the backend:
  - `filter`
  - `search`
  - `sort`
  - `pagination`
  - `scope`
  - optional research context (`source_table_id`, `source_table_scope`, `related_tables_mode`)
- Research values are best treated as a follow-up call:
  - `query` returns lead rows plus research linkage metadata
  - `research` returns `research_data_by_id` keyed by lead id for one `source_table_id`

Examples:

```bash
autotouch leads recipe --type create --out-file lead.json
autotouch leads create --data-file lead.json
autotouch leads get --lead-id <LEAD_ID>
```

```bash
autotouch leads recipe --type update --out-file lead-patch.json
autotouch leads update \
  --lead-id <LEAD_ID> \
  --data-file lead-patch.json
```

```bash
autotouch leads recipe --type contact_upsert --out-file contacts.json
autotouch leads upsert-contact-channels \
  --lead-id <LEAD_ID> \
  --data-file contacts.json
```

`contacts.json` example:

```json
{
  "emails": [
    { "address": "john@acme.com", "type": "work", "primary": true }
  ],
  "phones": [
    { "number": "+14155551234", "type": "mobile", "primary": true }
  ]
}
```

```bash
autotouch leads update-status \
  --filter-json '{"root":{"kind":"rule","field":"lead_status","operator":"eq","value":"new"}}' \
  --status contacting
```

```bash
autotouch leads reassign-owner \
  --lead-id <LEAD_ID_1> \
  --lead-id <LEAD_ID_2> \
  --user-id <USER_ID>
```

```bash
autotouch leads query \
  --filter-json '{"root":{"kind":"rule","field":"status","operator":"eq","value":"new"}}' \
  --limit 25
```

```bash
autotouch leads count \
  --search "fintech" \
  --scope org
```

```bash
autotouch leads stats \
  --source-table-id <TABLE_ID> \
  --source-table-scope company
```

```bash
autotouch leads research \
  --lead-id <LEAD_ID_1> \
  --lead-id <LEAD_ID_2> \
  --source-table-id <TABLE_ID>
```

```bash
autotouch leads filters-metadata
autotouch leads research-tables
```

## Task commands

The CLI now exposes the public task queue endpoints under `autotouch tasks`.

Use this pattern:

1. `autotouch tasks query` to page through tasks with the same `TaskQueryRequest` payload used by the API.
2. `autotouch tasks get` when you need one fully hydrated task by id.
3. `autotouch tasks create` for direct task creation from a JSON payload.
4. `autotouch tasks update` / `autotouch tasks bulk-update` for task status, owner, due date, and content changes.
5. `autotouch tasks draft` when you need just-in-time AI generation for a manual-capable task.
6. `autotouch tasks schedule-email` to schedule or immediately send a personal/manual email task.
7. `autotouch tasks delete` / `autotouch tasks bulk-delete` to soft-delete tasks by marking them skipped.

Important contract notes:
- Mutating task commands accept `--actor-user-id` for developer-key actor resolution.
- Query/count/assignee-counts use the backend `TaskQueryRequest` JSON shape directly.
- `draft` supports `--force` if you want to re-generate content even when a task already has content.
- `schedule-email` expects the same payload as `POST /api/task-queue/{task_id}/email/schedule`.
- AI draft outputs follow the same step behavior as sequences:
  - manual new email: subject + body
  - manual reply email: reply body
  - LinkedIn connect: connection note
  - LinkedIn message: message copy
  - call: call script

Examples:

```bash
autotouch tasks query --data-file task-query.json
autotouch tasks count --data-file task-query.json
autotouch tasks assignee-counts --data-file task-query.json
```

```bash
autotouch tasks get --task-id <TASK_ID>
autotouch tasks stats
```

```bash
autotouch tasks create --data-file task-create.json --actor-user-id <USER_ID>
autotouch tasks update --task-id <TASK_ID> --data-file task-update.json --actor-user-id <USER_ID>
```

```bash
autotouch tasks draft --task-id <TASK_ID> --force --actor-user-id <USER_ID>
autotouch tasks schedule-email --task-id <TASK_ID> --data-file schedule.json --actor-user-id <USER_ID>
```

```bash
autotouch tasks bulk-update --data-file task-bulk-update.json --actor-user-id <USER_ID>
autotouch tasks bulk-delete --data-file task-bulk-delete.json --actor-user-id <USER_ID>
```

## Bulk job status contract (authoritative run state)

Use bulk jobs as the source of truth for run lifecycle:

```bash
autotouch jobs get --job-id <JOB_ID> --output json
```

Important fields:
- `status`: `queued` | `distributing` | `processing` | `completed` | `partial` | `cancelled` | `error`
- `processed_rows`: successful rows
- `error_rows`: error rows
- `skipped_rows`: intentionally skipped/ineligible rows
- `total_rows`: scoped row count for the run
- `pending_batches`: derived remaining batches
- `terminal_reason`: terminal classifier
- timing checkpoints: `enqueued_at`, `orchestrator_started_at`, `provider_started_at`, `provider_completed_at`

Terminal states:
- `completed`
- `partial`
- `cancelled`
- `error`

Agent rule:
- Do not infer completion from local process output alone.
- Use `jobs get` counters + terminal status as canonical truth.
- If `job_id` is missing from local logs, recover it with `jobs list` filtered by `table_id` + `column_id`.

## Column create payload recipes (CLI-ready)

All examples below are used with:

```bash
autotouch columns create --table-id <TABLE_ID> --data-file <payload.json>
```

Recommendation:
- For provider-backed or generated columns, start with `autotouch columns recipe`.
- Built-in recipe types now include `formatter`, `llm_enrichment`, `email_finder`, `phone_finder`, `lead_finder`, `add_to_crm`, `sync_to_table`, and `add_to_sequence`.
- Email/phone enrichment does not require creating `add_to_crm` first.
- `add_to_crm` is an optional, non-billable export action.

### 1) `email_finder`

`email-finder.json`:

```json
{
  "key": "work_email",
  "label": "Work Email",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "email_finder",
  "autoRun": "never",
  "config": {
    "provider": "smart_email_finder",
    "strategy": "cost_optimized",
    "lookupStrategy": "linkedin",
    "linkedinOnly": true,
    "enableProfileFallback": false,
    "linkedinUrl": "linkedin_url"
  }
}
```

### 2) `phone_finder`

`phone-finder.json`:

```json
{
  "key": "mobile_phone",
  "label": "Mobile Phone",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "phone_finder",
  "autoRun": "never",
  "config": {
    "provider": "smart_phone_finder",
    "firstName": "first_name",
    "lastName": "last_name",
    "company": "company",
    "linkedinUrl": "linkedin_url"
  }
}
```

### 3) `lead_finder`

`lead-finder.json`:

```json
{
  "key": "lead_contacts",
  "label": "Lead Contacts",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "ai",
  "autoRun": "never",
  "config": {
    "provider": "lead_finder",
    "sourceMode": "bulk_companies",
    "companyDomain": "domain",
    "strictness": "flexible_roles",
    "storeAsLeads": true,
    "autoEnrichEmails": true,
    "autoEnrichPhones": false,
    "jobTitles": ["Head of Sales", "VP Sales"],
    "locations": ["United States"],
    "maxResults": 10
  }
}
```

### 4) `llm_enrichment`

Recommended for `agent` mode: use `instructions` and let the API compile the runnable prompt. In `basic` mode, write the prompt directly and keep it tailored to the user goal/preferences.

`llm-enrichment.json`:

```json
{
  "key": "company_research",
  "label": "Company Research",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "ai",
  "autoRun": "never",
  "config": {
    "instructions": "Research the company in this row and return JSON with icp_fit, summary, and risks.",
    "mode": "agent",
    "temperature": 0.7,
    "batchSize": 50,
    "promptSource": "generated",
    "useCompanyContext": true,
    "structuredOutput": true,
    "useAutoSchema": true
  }
}
```

Notes:
- In `agent` mode, `instructions` is the recommended authoring surface.
- The API compiles those instructions into the stored runnable prompt on create/update.
- Company/requester context is always enabled for generated `agent`-mode prompts.
- In `basic` mode, the recommended path is a hand-authored prompt. Use explicit placeholders like `{{company_name}}`, `{{domain}}`, or requester context placeholders from `autotouch context resolved` when the prompt should reference row/requester data.

Prompt variables:
- Row variables come from the current table row, for example `{{company_name}}`, `{{domain}}`, `{{first_name}}`, `{{linkedin_url}}`.
- Requester/company-context variables come from onboarding + org/personal context, for example `{{user_company_name}}`, `{{user_value_proposition}}`, `{{buyer_persona}}`, `{{user_persona}}`.
- The runtime only injects variables that the prompt explicitly references. If the prompt needs row/context data, include those `{{...}}` placeholders directly.
- Use `autotouch context resolved` to inspect the requester/company-context variables available for the current user/session.

Basic-mode example:

```json
{
  "key": "fit_summary",
  "label": "Fit Summary",
  "kind": "enrichment",
  "dataType": "text",
  "origin": "ai",
  "autoRun": "never",
  "config": {
    "mode": "basic",
    "prompt": "Write a short fit summary for {{company_name}} (website {{domain}}) and explain why it matches {{user_value_proposition}}.",
    "useCompanyContext": true
  }
}
```

If you provide a custom schema, use strict Schema V2 field-map shape:

```json
{
  "key": "district_it_contact",
  "label": "District IT Contact",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "ai",
  "autoRun": "never",
  "config": {
    "provider": "xai",
    "mode": "agent",
    "structuredOutput": true,
    "useAutoSchema": false,
    "response_schema": {
      "first_name": "string",
      "last_name": "string",
      "title": "string",
      "email": "string",
      "evidence_sources": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "type": "string",
            "url": "string",
            "snippet": "string"
          }
        }
      },
      "reasoning": "string"
    },
    "user_schema": {
      "first_name": "string",
      "last_name": "string",
      "title": "string",
      "email": "string",
      "evidence_sources": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "type": "string",
            "url": "string",
            "snippet": "string"
          }
        }
      },
      "reasoning": "string"
    }
  }
}
```

Schema guardrails:
- Root must be a field map (no root `{"type":"object","properties":...}` wrapper).
- Arrays must be explicit `{"type":"array","items":...}`.
- Never use list-literal schema values like `["string"]` or `[{...}]`.
- Prefer snake_case schema keys (`response_schema`, `user_schema`, `use_auto_schema`); camelCase variants are accepted in compatibility paths.

### 5) `formatter`

`formatter.json`:

```json
{
  "key": "engagement_statement",
  "label": "Engagement Statement",
  "kind": "formatter",
  "dataType": "text",
  "origin": "manual",
  "autoRun": "onSourceUpdate",
  "config": {
    "formula": "return (row['first_name'] || '') + ' ' + (row['last_name'] || '') + \" reacted to David Wilkins' post: \\\"\" + (row['post_content'] || '') + \"\\\"\";",
    "sourceColumns": ["first_name", "last_name", "post_content"]
  }
}
```

Notes:
- Formatter formulas must reference `row` keys (`row['first_name']`, `row.last_name`).
- Bare template placeholders like ``${first_name}`` are invalid and rejected.
- Formatter columns are source-driven; backend policy normalizes formatter `autoRun` to `onSourceUpdate`.

### 6) `add_to_crm`

`add-to-crm.json`:

```json
{
  "key": "add_to_leads",
  "label": "Add to Leads",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "manual",
  "autoRun": "onSourceUpdate",
  "config": {
    "provider": "add_to_crm",
    "leadSource": "research_table_export",
    "fieldMappings": {
      "mode": "single",
      "linkedinUrl": "linkedin_url",
      "companyDomain": "domain",
      "firstName": "first_name",
      "lastName": "last_name",
      "title": "title",
      "companyName": "company",
      "emailAddresses": [
        { "column": "work_email", "type": "work" }
      ],
      "phoneNumbers": [
        { "column": "mobile_phone", "type": "mobile" }
      ]
    },
    "sourceColumns": [
      "linkedin_url",
      "domain",
      "first_name",
      "last_name",
      "title",
      "company",
      "work_email",
      "mobile_phone"
    ]
  }
}
```

Add-to-Leads note: `companyDomain` is required; LinkedIn is optional. Each row still needs at least one usable hard identity signal (LinkedIn, email, or phone). Names and location fields are metadata only for this flow. Run is non-billable.
If `companyDomain` is missing in the table, derive or enrich that domain column first, then rerun `add_to_crm`.
Queued lifecycle: the CLI/API accepts the run immediately, then the backend hands off `ops -> data_io` for execution.
Creating `add_to_crm` after upstream email/phone/lead columns already finished does not replay those older updates. In that case, run `add_to_crm` explicitly or rerun the upstream source column.

CRM data model expectations (recommended before `add_to_crm`):
- Lead identity/dedupe expects `company_domain` plus one usable hard identity signal. `linkedin_url` is a strong optional signal, not a requirement.
- Placeholder or malformed hard identifiers are treated as missing during research-table export. Rows without any surviving LinkedIn/email/phone signal are skipped instead of erroring.
- `company_domain` is required; `company_name` is only a display hint and is applied to the linked Company record when provided.
- Lead records link to Company via `company_id`; company names live on Company docs, not as canonical lead fields.
- Canonical contact fields are arrays (`email_addresses[]`, `phone_numbers[]`); top-level `email`/`mobile_number` may exist on legacy rows but should not be treated as source of truth.
- Reference docs:
  - `docs/data/leads.md`
  - `docs/data/companies.md`

### 7) `sync_to_table`

`sync-to-table.json`:

```json
{
  "key": "sync_to_table",
  "label": "Sync to Table",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "manual",
  "autoRun": "onSourceUpdate",
  "config": {
    "provider": "sync_to_table",
    "destinationTableId": "<DESTINATION_TABLE_ID>",
    "columnMappings": [
      { "sourceKey": "company", "destKey": "company" },
      { "sourceKey": "domain", "destKey": "company_domain" },
      { "sourceKey": "work_email", "destKey": "work_email" }
    ]
  }
}
```

Notes:
- Single-destination mode uses `destinationTableId` + `columnMappings`.
- Router mode is also supported with `config.routes[]` (top-to-bottom matching, first hit wins, default route catches unmatched rows).
- Route conditions are evaluated against the parent/source row, not against each list item.
- `autoRun: "onSourceUpdate"` means dependency-driven runs from source row inserts/updates. Saving the column does not backfill existing rows for `sync_to_table`; use an explicit column run/backfill when needed.
- List mode uses `syncMode: "list"` with `listSourceColumnId` and optional `listPath` (default `items`) to expand a canonical JSON payload like `{ "items": [...], "reasoning": "..." }`.
- In list mode:
  - `sourceScope: "item"` maps fields from each `items[]` object.
  - `sourceScope: "row"` maps fields from the parent/source row.
  - Explicit item mappings are still recommended for renamed fields such as `name -> full_name`.
- Blank placeholder list items are skipped and do not create destination rows.

### 8) `add_to_sequence`

`add-to-sequence.json`:

```json
{
  "key": "add_to_sequence",
  "label": "Add to Sequence",
  "kind": "enrichment",
  "dataType": "json",
  "origin": "manual",
  "autoRun": "onSourceUpdate",
  "config": {
    "provider": "add_to_sequence",
    "sequenceId": "<SEQUENCE_ID>",
    "sourceLeadColumn": "add_to_leads"
  }
}
```

Notes:
- `sourceLeadColumn` must be the source column key that stores lead IDs (for example `add_to_leads` or another lead-id column key from `lead_finder` output), not the provider name `add_to_crm`.
- `sequenceId` is the target sequence workflow ID.
- The target sequence must already be `ACTIVE` for real enrollment. Table `add_to_sequence` runs and direct `POST /api/sequences/{id}/enroll` share the same activation check.
- Common tail order after contact rows or lead IDs exist is `email_finder -> add_to_crm -> create/activate sequence -> add_to_sequence`.
- Creating `add_to_sequence` after lead IDs already exist does not replay those older updates. In that case, run `add_to_sequence` explicitly or rerun the lead-id source column.
- `add_to_sequence` runs auto-attach `research_context.source_table_id` (and table name when available). Field selection stays implicit so sequence drafts/audience resolve favorites from current starred columns by default.
- Star/favorite the highest-signal fields so callers can see them quickly in the sidecar during live call workflows.
- The same favorite set is also the default AI drafting context when explicit `fieldIds` are not provided.

## Example pipeline: LLM items -> contacts table -> leads -> sequence

Use this when an LLM column returns canonical JSON like `{ "items": [...] }` and each item should become a contact row.
This is an example pattern, not the default recommendation for all person discovery.
Use `lead_finder` first when the goal is explicit buyer/lead discovery with clear role targeting at scale.
Use agent `llm_enrichment` when the workflow needs custom research output, website-discovered contacts, or low-coverage / hard-to-find cases.

1. Create the source table and import company rows.
2. Create the LLM enrichment column that outputs `items[]`.
3. Create `sync_to_table` in `list` mode:
   - set `listSourceColumnId` to the LLM column ID
   - keep item fields on `sourceScope: "item"`
   - keep parent company metadata on `sourceScope: "row"`
4. Run or wait for `sync_to_table` so the destination contacts table has rows.
5. On the destination contacts table, create and run `email_finder`.
6. Create `add_to_crm` on that same destination contacts table.
7. If email finder already finished before `add_to_crm` existed, run `add_to_crm` explicitly.
8. Create the target sequence and set it `ACTIVE`.
9. Create `add_to_sequence` with `sourceLeadColumn` set to the lead-id column key, usually `add_to_leads`.
10. If leads already existed before `add_to_sequence` was created, run `add_to_sequence` explicitly.

## Common workflow

```bash
autotouch capabilities
autotouch tables create --name "CLI Contacts"
autotouch rows import-csv --table-id <TABLE_ID> --file contacts.csv
autotouch columns recipe --type add_to_crm --out-file column.json
autotouch columns create --table-id <TABLE_ID> --data-file column.json
autotouch sequences recipe --type create --out-file sequence.json
autotouch sequences create --data-file sequence.json
autotouch sequences activate --sequence-id <SEQUENCE_ID>
autotouch columns recipe --type add_to_sequence --out-file add-to-sequence.json
autotouch columns create --table-id <TABLE_ID> --data-file add-to-sequence.json
autotouch columns run-next --table-id <TABLE_ID> --column-id <COLUMN_ID> --count 25 --filters-file filters.json --show-estimate --wait
autotouch jobs get --job-id <JOB_ID>
```

Sequence creation note:
- The CLI can create/update/activate sequence definitions directly with `autotouch sequences ...`.
- Research-table `add_to_sequence` columns and direct `autotouch sequences enroll` both target an existing sequence.
- Real enrollment still requires that sequence to be `ACTIVE` unless direct enroll uses `reviewOnly=true`.
- Downstream task queue work is available through `autotouch tasks ...`.

## Safe run patterns (`firstN` + `--unprocessed-only`)

Use this pattern for progressive rollouts.

```bash
# Pilot first 10 rows
autotouch columns run \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --scope firstN \
  --first-n 10 \
  --unprocessed-only \
  --show-estimate \
  --wait

# Extend to first 15 rows (processes the next 5 if first 10 are already done)
autotouch columns run \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --scope firstN \
  --first-n 15 \
  --unprocessed-only \
  --show-estimate \
  --wait
```

Notes:
- `firstN` without `--unprocessed-only` can re-run already-processed rows.
- With `--unprocessed-only`, `firstN` means "first N currently eligible unprocessed rows", not "exactly N new rows since your last check".
- If you need an exact count (for example exactly 5 rows), use `run-next` below.
- Run-scope rule of thumb: use `row` for one exact ID, `subset` for exact many IDs, `filtered` for the current filtered view, `firstN` for staged rollouts, and `all` for full-table runs.
- `--wait` polls `/api/bulk-jobs/{job_id}` until terminal status.
- If a job stays `queued`, workers for that provider queue may be scaled to `0`.
- During execution, non-final batches remain `processing`; they should not be treated as complete.

## Exact count runs (`run-next`)

Use this when you need exactly `N` rows in one run.
The CLI selects candidate row IDs first, then executes `/run` with `scope=subset`.

```bash
# Run exactly 5 unprocessed rows from the current view
autotouch columns run-next \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --count 5 \
  --filters-file filters.json \
  --show-estimate \
  --wait
```

Notes:
- Default behavior is unprocessed-only selection.
- Add `--include-processed` to allow already-processed rows into candidate selection.
- `run-next` is deterministic on count (subject to available eligible rows).
- If fewer than `N` eligible rows exist, it runs the available subset and reports selected count.

### Agent execution contract (strict)

When operating this CLI as an agent, use backend job state as source of truth:

1. Treat a run as started only if `/run` returns a `jobId` (`job_id`).
2. Treat a run as completed only when `GET /api/bulk-jobs/{job_id}` returns terminal status.
3. Never infer progress/completion from local process liveness alone.
4. If polling is blocked by local network/approval/sandbox constraints, report "run state not confirmed" (do not claim still running/completed).
5. If polling returns `not_found` or `unknown_not_found`, treat that run as failed/ambiguous and verify row state before retry.

Agent output contract:
- Prefer `--output json` (and `--compact` when token budget matters).
- Parse machine fields only (`job_id`, `status`, `processed_rows`, `error_rows`, `skipped_rows`, `total_rows`).
- Do not infer success from human-readable log lines.
- If response parsing fails, treat run state as unknown and recover via `autotouch jobs list` + `autotouch jobs get`.

### Enrichment value parsing contract (phone + email)

When summarizing enrichment results, use this sequence:

1. Inspect raw outputs first (at least 3 sample rows).
2. Choose parser mode from column `dataType`.
3. For `dataType=json`, apply key precedence below.
4. For scalar types (`text`, `number`, `date`, `boolean`, `email`, `url`), read direct scalar values (no JSON key-path parsing).

For JSON outputs, parse value payloads by precedence instead of a single key.

Phone value extraction order:

1. `mobile_number`
2. `phone_numbers[0].number`
3. `primary_phone`

Email value extraction order:

1. `response`
2. `email`
3. `work_email`

Important:
- Do not treat missing `response`/`phone` as a hard miss for phone finder.
- If top-level path is missing, continue to fallback paths before reporting `not_found`.
- Validate the parser against a few raw sample rows before publishing counts.

JSON split note:
- `columns projections` is optional by default.
- Use it when downstream filtering/mapping/sequence variable binding needs stable flat keys.
- Creating the projection is enough for existing rows; the backend backfills/materializes those values automatically.
- If source columns are JSON enrichments (email/phone/LLM), run the source column first with `--wait` and confirm terminal job status before splitting.
- CLI behavior: `columns projections` will emit preflight warnings when a JSON enrichment source appears unrun/unverified.
- Warning output contract: when warnings exist, JSON output is wrapped as `{ "event": "projections.created_with_warnings", "warnings": [...], "result": <api_response> }`.

Reference playbook + runbook:
- `docs/research-table/guides/context-first-sequence-playbook.md`
- `docs/research-table/reference/runbooks/context-first-sequence.json`

### Wait output contract (CLI >= 0.2.11)

`columns run --wait` and `columns run-next --wait` now emit structured lifecycle events:

- `run.wait_started`
- `job.progress`
- `run.completed` / `run.timed_out`

Run outputs include:

- `job_id`
- `job_status_url`
- `watch_command` (copy-paste fallback for explicit polling)

Terminal status values:

- `completed`
- `partial`
- `error`
- `cancelled`

CLI-protected failure statuses:

- `not_found`
- `unknown_not_found`

Non-terminal status values:

- `queued`
- `distributing`
- `processing`

Recommended fields to read from `jobs get` while waiting:
- `processed_rows`
- `error_rows`
- `skipped_rows`
- `total_rows`
- `pending_batches`
- `terminal_reason`

### Canonical fallback (when `--wait` is noisy in your runtime)

```bash
# 1) Queue run and capture jobId
autotouch columns run \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --scope firstN \
  --first-n 15 \
  --unprocessed-only \
  --show-estimate \
  --output json

# 2) If jobId was not captured, recover latest from backend history
autotouch jobs list \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --limit 1 \
  --output json

# 3) Poll backend truth directly
autotouch jobs get --job-id <JOB_ID> --output json
```

Repeat `jobs get` until status is terminal.

## CSV import (agent-safe, async-first)

`rows import-csv` defaults to optimized import transport (`/import-optimized`) so large files do not fail on a single long request.

```bash
# Queue background import and return task_id quickly
autotouch rows import-csv \
  --table-id <TABLE_ID> \
  --confirm-table-id <TABLE_ID> \
  --file contacts.csv \
  --checkpoint-file .autotouch-import.json

# Wait for completion
autotouch rows import-status \
  --table-id <TABLE_ID> \
  --task-id <TASK_ID> \
  --wait
```

Notes:
- Use `--sync` only when you explicitly want synchronous behavior.
- Legacy direct path is still available with `--transport direct`.
- Optimized import emits progressive events while processing, and starts with a small first batch for fast initial row visibility.
- Use `--dry-run` for parse-only preview before writing rows.
- Use `--validate-only` to test server-side CSV parsing/shape without writing rows.
- Blacklist controls on optimized import:
  - company-domain filtering is ON by default
  - email filtering is OFF by default
  - disable company filtering with `--no-check-company-blacklist`
  - enable email filtering with `--check-email-blacklist`
- Safety assertions are available on import:
  - `--expected-rows <N>`
  - `--require-columns col_a,col_b`
  - `--duplicate-key col_a,col_b`
  - `--require-non-empty post_content:0.95`
- Assertions run as preflight checks. For async imports with `--wait`, postflight verification runs against the persisted task manifest automatically.
- Use `--allow-reimport` only when intentionally importing the same file again.
- Import responses include `blacklist_summary` with company/email `skipped_count` and `enforced` flags.

Safe protocol:

```bash
# 1) Validate parse/shape only (no writes)
autotouch rows import-csv --table-id <TABLE_ID> --confirm-table-id <TABLE_ID> --file contacts.csv --validate-only

# 2) Import with strict assertions + wait
autotouch rows import-csv \
  --table-id <TABLE_ID> \
  --confirm-table-id <TABLE_ID> \
  --file contacts.csv \
  --checkpoint-file .autotouch-import.json \
  --check-email-blacklist \
  --expected-rows 57 \
  --require-columns first_name,last_name,post_content \
  --duplicate-key linkedin_url,post_url \
  --require-non-empty post_content:1 \
  --wait

# 3) Re-verify later (optional)
autotouch rows import-verify --table-id <TABLE_ID> --task-id <TASK_ID> --expected-rows 57

# 4) Roll back by task_id if needed
autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID>
```

Manage blacklist entries (native CLI, admin identity required):

```bash
# List current entries
autotouch blacklist list --type-filter all --limit 100

# Add entries
autotouch blacklist add --type domain --value competitor.com --reason "Do not contact"
autotouch blacklist add --type email --value blocked@example.com --reason "Unsubscribed"

# Bulk-import entries from CSV/TXT
autotouch blacklist import --file blacklist.csv

# Check or filter email sets (auto-chunked for large lists)
autotouch blacklist check --emails-file recipients.csv --emails-column email --summary-only
autotouch blacklist filter --emails-file recipients.csv --emails-column email --summary-only
```

Recommended timing (cost + ICP guardrail):
- Add known suppressions early (unsubscribed addresses, do-not-contact domains, existing customers, competitors, and clear non-ICP targets).
- Before billable enrichments (`llm_enrichment`, `email_finder`, `phone_finder`), run blacklist filtering on candidate emails and enrich only clean rows.
- Run a final blacklist check/filter again before downstream outreach or dialing.

```bash
# Example: pre-enrichment blacklist gate
autotouch blacklist filter \
  --emails-file candidates.csv \
  --emails-column work_email \
  --output json
```

## Capabilities for agents

Use capabilities as the source of truth before generating payloads:

```bash
autotouch capabilities
```

Agent expectations:

- `column_types` tells you which column types are runnable and which are non-billable transforms (`json_split`, `formatter_formula`).
- `filtering` describes valid scope/filter semantics for estimate/run.
- `automation.auto_run` describes supported auto-run modes + config field names.
- `automation.providers` exposes provider-specific setup contracts, save-time backfill behavior, dispatch strategy, and recipe type names for formatter/LLM/finders/action columns.
- `execution_policies.llm.output_contract` describes output behavior by mode:
  - `agent` => JSON-oriented structured output
  - `basic` => text or JSON (`dataType=json` for structured JSON output)
- `webhooks.table_ingest` describes webhook ingest auth contract (metadata only; no secret tokens).

For a built-in machine-readable run playbook, use:

```bash
autotouch sop --output json
```

## JSON output pipeline pattern

For enrichment responses that return structured JSON, use this chain:

1. Run enrichment into a JSON column.
2. Wait for terminal status (`completed`/`partial`/`error`/`cancelled`) using `--wait` or `jobs get/watch`.
3. Split JSON into projection columns (optional; only when stable flat keys are needed).
4. Optional formatter normalization.
5. Feed extracted/normalized keys into downstream enrichment columns.

Important mode distinction:

- Agent mode is JSON-oriented and is intended for structured outputs.
- Basic mode can return plain text or JSON depending on your column `dataType`/schema setup.

### Recommended ICP buyer pattern (agent mode)

For go-to-market workflows, prefer one best-fit buyer per row in this stage.

- Ask for exactly one person (not a list/array) with a flat JSON object.
- Recommended keys: `first_name`, `last_name`, `title`, `company_name`, `company_website`, `linkedin_url`.
- Then split those keys into projection columns and run email/phone enrichment on those outputs.
- Use `lead_finder` first for larger companies when role ownership is clear; use agent research for hard-to-find or low-coverage cases.
- Target responsibilities, not exact titles (for example: "most likely responsible for buying social/cell engagement software").

Prompt shape recommendation:

```text
Find the single most likely buyer of cell engagement software for this company.
Return exactly one JSON object with keys:
first_name, last_name, title, company_name, company_website, linkedin_url.
Use empty string when unknown.
```

Full strategy and examples:

- `docs/research-table/guides/icp-buyer-discovery.md`

```bash
# Create projections from a JSON source column
autotouch columns projections \
  --table-id <TABLE_ID> \
  --data-file projections.json

# Optional: update downstream column config to reference projected keys
autotouch columns update \
  --table-id <TABLE_ID> \
  --column-id <DOWNSTREAM_COLUMN_ID> \
  --data-file column-update.json
```

`projections.json`:

```json
{
  "items": [
    {
      "key": "person_name",
      "label": "Person Name",
      "sourceColumnId": "<JSON_COLUMN_ID>",
      "path": "person_name",
      "dataType": "text"
    },
    {
      "key": "school_domain",
      "label": "School Domain",
      "sourceColumnId": "<JSON_COLUMN_ID>",
      "path": "school_domain",
      "dataType": "text"
    }
  ]
}
```

### First principles: intent + context

For most workflows, useful output has two layers:
- context: source evidence (what happened / what was observed)
- intent: interpretation of that evidence (what to prioritize / do next)

Design guidance:
- preserve full-fidelity context in at least one raw field (for example full post/body text)
- default behavior for agents: write full context/content, not summaries or truncation
- only truncate/summarize when there is a hard limit (storage/model/provider/payload), and mark that it was truncated
- keep intent separate from raw context so automation can evolve without data loss
- for calling workflows, star/favorite high-signal fields so sidecar context is immediately useful without extra clicks
- for AI-generated emails/copy, keep intent + full context together in imports so drafts are grounded in source evidence
- keep one entity per row and dedupe using a stable identity key
- treat snippets/summaries as optional derived fields, never the source of truth
- if automation depends on intent values, use a small normalized label taxonomy

Optional field pattern (adapt as needed):
- `post_content` or `context_raw`: full long-form context text
- `context_url`: source URL
- `context_timestamp`: recency marker
- `intent_label`: normalized intent category
- `intent_reason`: human-readable explanation

CSV handling note:
- long context fields may include newlines/commas and are valid when properly quoted
- run `autotouch rows import-csv --validate-only` (or `--dry-run`) first
- for strict guardrails, add assertions: `--expected-rows`, `--require-columns`, `--duplicate-key`, `--require-non-empty`
- for mutating imports, use `--confirm-table-id` and `--checkpoint-file` to reduce accidental corruption/duplicates
- import does not intentionally truncate text values; practical limits are the underlying MongoDB document-size limits

Multiline `post_content` examples:

```csv
# bad (unquoted multiline content breaks row shape)
first_name,linkedin_url,post_content
Ada,https://linkedin.com/in/ada,Line 1
Line 2
```

```csv
# good (quoted multiline content is valid CSV)
first_name,linkedin_url,post_content
Ada,https://linkedin.com/in/ada,"Line 1
Line 2"
```

Do not re-import blind (recovery flow):
- stop and keep the original `task_id`
- inspect status: `autotouch rows import-status --table-id <TABLE_ID> --task-id <TASK_ID>`
- prove postflight: `autotouch rows import-verify --table-id <TABLE_ID> --task-id <TASK_ID> ...assertions...`
- if verification fails, preview rollback: `autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID> --dry-run`
- then rollback: `autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID>`
- fix CSV quoting/mapping and run `--validate-only` before any new import

## Filtering (credit control)

Use `scope=filtered` to run only matching rows.

`filters.json`:

```json
{
  "mode": "and",
  "filters": [
    { "columnKey": "linkedin_url", "operator": "isNotEmpty" },
    { "columnKey": "country", "operator": "equals", "value": "United States" }
  ]
}
```

```bash
# Estimate first (non-billable)
autotouch columns estimate \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --scope filtered \
  --filters-file filters.json \
  --unprocessed-only

# Run same payload with rollout cap
autotouch columns run \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --scope filtered \
  --filters-file filters.json \
  --unprocessed-only \
  --first-n 200 \
  --show-estimate --wait
```

### Cost tip: filter out empty rows between enrichments

Most teams run paid enrichments only on rows that already have required upstream data.
This avoids spending credits on rows that cannot produce useful results yet.

Example: run email finder only when `linkedin_url` exists and `work_email_address` is still empty.

```json
{
  "mode": "and",
  "filters": [
    { "columnKey": "linkedin_url", "operator": "isNotEmpty" },
    { "columnKey": "work_email_address", "operator": "isEmpty" }
  ]
}
```

Pattern to reuse:
- Step 1: create/select a filter that excludes empty prerequisite fields.
- Step 2: run small (`firstN` or `run-next`) with `--show-estimate`.
- Step 3: expand only after output quality looks good.

### Cost tip: run blacklist gate before billable enrichments

Before `llm_enrichment`, `email_finder`, or `phone_finder`, run blacklist check/filter so credit spend stays focused on eligible ICP rows (and excludes suppressions like customers/competitors).

```bash
autotouch blacklist filter \
  --emails-file candidates.csv \
  --emails-column work_email \
  --output json
```

## Auto-run configuration

Auto-run is set on the column definition (`autoRun`) and can be changed later with `columns update`.

Formatter-specific rule:
- Formatter columns are always normalized to `autoRun: "onSourceUpdate"` by the backend.
- Attempting to set formatter `autoRun` to `never` or `onInsert` is ignored/overridden server-side.

`column-update.json`:

```json
{
  "autoRun": "onInsert",
  "config": {
    "autoRunMode": "conditional",
    "autoRunFilters": {
      "mode": "and",
      "filters": [
        { "columnKey": "linkedin_url", "operator": "isNotEmpty" }
      ]
    }
  }
}
```

```bash
autotouch columns update \
  --table-id <TABLE_ID> \
  --column-id <COLUMN_ID> \
  --data-file column-update.json
```

## Table webhooks (ingest)

Webhook ingestion uses a per-table token, not developer API key scopes.

```bash
# Read current webhook config
autotouch webhooks get --table-id <TABLE_ID>

# Create/rotate token (token is returned once)
autotouch webhooks rotate --table-id <TABLE_ID>
```

`records.json`:

```json
{
  "records": [
    { "first_name": "Ada", "email": "ada@example.com" }
  ]
}
```

```bash
# Send records with webhook token
autotouch webhooks ingest \
  --table-id <TABLE_ID> \
  --records-file records.json \
  --webhook-token <WEBHOOK_TOKEN>
```

## Outbound webhook subscriptions

Use outbound subscriptions to receive business events (`bulk_job.*`, `lead.*`, `sequence_enrollment.created`, `task.created`).

```bash
# List subscriptions
autotouch webhooks subscriptions list

# Create subscription
autotouch webhooks subscriptions create \
  --url https://example.com/webhooks/smart-table \
  --events bulk_job.* lead.status_changed task.created

# Pause/resume
autotouch webhooks subscriptions pause --subscription-id <SUBSCRIPTION_ID>
autotouch webhooks subscriptions resume --subscription-id <SUBSCRIPTION_ID>

# Rotate signing secret
autotouch webhooks subscriptions rotate-secret --subscription-id <SUBSCRIPTION_ID>

# Fire test event
autotouch webhooks subscriptions test \
  --subscription-id <SUBSCRIPTION_ID> \
  --event-type lead.created \
  --data-json '{"ping":"ok"}'

# Inspect deliveries + attempts
autotouch webhooks deliveries list --subscription-id <SUBSCRIPTION_ID> --limit 50
autotouch webhooks deliveries attempts --delivery-id <DELIVERY_ID>
```

Required scopes for developer API keys:
- `webhooks:read` (list/get/deliveries)
- `webhooks:write` (create/update/delete/pause/resume/rotate/test)

Retention note:
- Webhook event cache and delivery-attempt logs default to 7 days
  (`WEBHOOK_EVENTS_TTL_DAYS`, `WEBHOOK_DELIVERY_ATTEMPTS_TTL_DAYS`).

## Budget and safety controls

```bash
# Estimate only (no execution)
autotouch columns run --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope filtered --filters-file filters.json --unprocessed-only --dry-run

# Guard against overspend
autotouch columns run --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope filtered --filters-file filters.json --unprocessed-only --max-credits 50

# Poll until terminal status
autotouch jobs watch --job-id <JOB_ID>

# Stop a running column
autotouch columns stop --table-id <TABLE_ID> --column-id <COLUMN_ID>
```

## Notes

- The CLI is a thin wrapper around the same HTTP endpoints documented in `docs/research-table/reference/tables-api.md`.
- If a key is missing scope, CLI commands fail the same way raw API calls do (`403`).
- Use `--base-url` and `--token` per command for CI/ephemeral environments.
