autotouch-cli 0.2.23__tar.gz → 0.2.25__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. autotouch_cli-0.2.25/PKG-INFO +1138 -0
  2. autotouch_cli-0.2.25/autotouch_cli.egg-info/PKG-INFO +1138 -0
  3. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/autotouch_cli.egg-info/SOURCES.txt +1 -1
  4. autotouch_cli-0.2.25/docs/research-table/reference/autotouch-cli.md +1129 -0
  5. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/pyproject.toml +2 -2
  6. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/smart_table_cli.py +38 -2
  7. autotouch_cli-0.2.23/PKG-INFO +0 -657
  8. autotouch_cli-0.2.23/autotouch_cli.egg-info/PKG-INFO +0 -657
  9. autotouch_cli-0.2.23/docs/research-table/reference/autotouch-cli-pypi.md +0 -648
  10. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/autotouch_cli.egg-info/dependency_links.txt +0 -0
  11. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/autotouch_cli.egg-info/entry_points.txt +0 -0
  12. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/autotouch_cli.egg-info/requires.txt +0 -0
  13. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/autotouch_cli.egg-info/top_level.txt +0 -0
  14. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/__init__.py +0 -0
  15. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/add_column_unique_index.py +0 -0
  16. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/attach_csv_import_leads_to_research_table.py +0 -0
  17. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/bundle_sequences_backend.py +0 -0
  18. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/check_agent_traces.py +0 -0
  19. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/check_column_mode.py +0 -0
  20. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/exit_terminal_leads_from_sequences.py +0 -0
  21. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/fetch_lead.py +0 -0
  22. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/fix_lead_titles_from_csv.py +0 -0
  23. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250106_add_column_position.py +0 -0
  24. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250108_fix_legacy_column_fields.py +0 -0
  25. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250109_add_user_fields_to_tables.py +0 -0
  26. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250117_add_call_logs_webhook_indexes.py +0 -0
  27. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250117_rename_call_logs_collection.py +0 -0
  28. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250119_create_leads_unique_email_index.py +0 -0
  29. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250123_add_filter_indexes.py +0 -0
  30. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250123_add_llm_responses_collection.py +0 -0
  31. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250128_migrate_user_ids_to_objectid.py +0 -0
  32. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250208_backfill_task_research_values.py +0 -0
  33. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250604_add_origin_indexes.py +0 -0
  34. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250608_cleanup_agent_metadata.py +0 -0
  35. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250608_rename_agent_metadata_to_metadata.py +0 -0
  36. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250922_add_activity_indexes.py +0 -0
  37. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250926_migrate_single_to_arrays.py +0 -0
  38. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250928_add_missing_timestamp_fields.py +0 -0
  39. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250929_add_task_join_indexes.py +0 -0
  40. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250929_add_task_join_indexes_safe.py +0 -0
  41. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20250929_create_shared_phone_cache.py +0 -0
  42. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20251007_add_rows_position_id_index.py +0 -0
  43. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20251109_add_ttl_for_llm_and_preview_traces.py +0 -0
  44. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20260113_normalize_table_filter_operators.py +0 -0
  45. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20260113_set_user_permissions_user_admin.py +0 -0
  46. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20260204_sync_lead_owner_from_tasks.py +0 -0
  47. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20260303_add_webhook_subscription_collections.py +0 -0
  48. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/20260305_force_formatter_autorun_on_source_update.py +0 -0
  49. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/migrate_org_user_credits.py +0 -0
  50. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/set_default_lead_status.py +0 -0
  51. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/migrations/update_lead_owner_from_tasks.py +0 -0
  52. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/reassign_sequence_owner.py +0 -0
  53. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/run_sidecar_orchestrator_demo.py +0 -0
  54. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/test_crm_company_policy.py +0 -0
  55. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/test_sequences_instantly_e2e.py +0 -0
  56. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/test_sequences_personal_e2e.py +0 -0
  57. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/test_task_error_logger.py +0 -0
  58. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/scripts/verify_azurite_voicemail.py +0 -0
  59. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/setup.cfg +0 -0
  60. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_contactout_custom.py +0 -0
  61. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_contactout_integration.py +0 -0
  62. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_contactout_multi_titles.py +0 -0
  63. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_contactout_pipeline.py +0 -0
  64. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_contactout_simple.py +0 -0
  65. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_contactout_v2_bulk.py +0 -0
  66. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_lead_required_fields.py +0 -0
  67. {autotouch_cli-0.2.23 → autotouch_cli-0.2.25}/tests/test_phone_provider_pipeline.py +0 -0
@@ -0,0 +1,1138 @@
1
+ Metadata-Version: 2.4
2
+ Name: autotouch-cli
3
+ Version: 0.2.25
4
+ Summary: Autotouch Smart Table CLI
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: requests>=2.31.0
8
+ Requires-Dist: python-dotenv>=1.0.0
9
+
10
+ # Autotouch CLI Reference (`autotouch`)
11
+
12
+ This page documents the installable CLI for the Smart Table developer API.
13
+ It is the canonical CLI doc in this repo and the package readme shipped to PyPI.
14
+ Use it when you want less boilerplate than raw HTTP/cURL while keeping the same API behavior, auth, and scopes.
15
+
16
+ ## Start here
17
+
18
+ Use this order when you are orienting in the CLI:
19
+
20
+ 1. Configure auth and confirm scopes with `autotouch auth check` and `autotouch capabilities`.
21
+ 2. Use the API endpoint -> CLI command map when translating an existing API workflow.
22
+ 3. Use `autotouch columns recipe` before creating provider-backed workflow columns.
23
+ 4. Use `autotouch jobs get` as the source of truth for async run state.
24
+ 5. Use raw HTTP for sequences/tasks workflows; those endpoints share the same developer-key model but do not yet have dedicated CLI commands.
25
+
26
+ ### Quick decision guide
27
+
28
+ | If you want to... | Start here |
29
+ | --- | --- |
30
+ | create a table or inspect available tables | `autotouch tables create` / `autotouch tables list` |
31
+ | import CSV data safely | `autotouch rows import-csv --validate-only`, then `autotouch rows import-csv --wait` |
32
+ | create a provider-backed column | `autotouch columns recipe --type <TYPE>`, then `autotouch columns create` |
33
+ | run exactly `N` rows | `autotouch columns run-next` |
34
+ | stage a gradual rollout | `autotouch columns run --scope firstN --unprocessed-only` |
35
+ | run only the current filtered segment | `autotouch columns run --scope filtered --filters-file ...` |
36
+ | run one exact row or an explicit list of row IDs | `scope=row` for one ID, `scope=subset` for many IDs |
37
+ | verify whether a job is really done | `autotouch jobs get --job-id <JOB_ID>` |
38
+ | create projections from JSON output | `autotouch columns projections` |
39
+ | work with sequences/tasks | raw HTTP plus `docs/platform/external-workflows-api.md` |
40
+
41
+ ### Operating model
42
+
43
+ - This file is the full CLI reference and the package readme published to PyPI.
44
+ - The installed package gives you CLI entrypoints and package metadata; do not assume there is a separate installed docs directory.
45
+ - Research-table APIs are the primary CLI surface today; workflow APIs (sequences/tasks) are still HTTP-first.
46
+ - For async operations, backend bulk-job state is authoritative; local terminal output is only a convenience layer.
47
+ - For staged or cost-sensitive runs, estimate first and prefer filtered scopes plus `firstN` or `run-next`.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pipx install autotouch-cli
53
+ # or
54
+ pip install autotouch-cli
55
+ ```
56
+
57
+ ## Configure auth
58
+
59
+ Developer keys and scopes are identical to raw API usage (`stk_...`, same scope checks).
60
+
61
+ ```bash
62
+ autotouch auth set-key --api-key stk_... --base-url https://app.autotouch.ai
63
+ autotouch auth check
64
+ autotouch auth show
65
+ ```
66
+
67
+ Credentials are stored in `~/.config/autotouch/config.json` by default.
68
+ Override path with `AUTOTOUCH_CONFIG_PATH`.
69
+
70
+ Developer key scope reference (including workflow scopes like `sequences:*` and `tasks:*`):
71
+ - `docs/platform/authentication.md`
72
+
73
+ ## Agent bootstrap (one-call signup + key)
74
+
75
+ If an agent/user does not have an account + developer key yet, bootstrap both in one call:
76
+
77
+ ```bash
78
+ curl -X POST "https://app.autotouch.ai/api/auth/agent-bootstrap" \
79
+ -H "Content-Type: application/json" \
80
+ -d '{
81
+ "first_name": "Ada",
82
+ "last_name": "Lovelace",
83
+ "email": "ada@yourcompany.com",
84
+ "password": "use-a-strong-random-password",
85
+ "organization_name": "Your Company",
86
+ "key_name": "Agent bootstrap key"
87
+ }'
88
+ ```
89
+
90
+ Then store the returned `apiKey`:
91
+
92
+ ```bash
93
+ autotouch auth set-key --api-key stk_... --base-url https://app.autotouch.ai
94
+ autotouch auth check
95
+ ```
96
+
97
+ Notes:
98
+ - New orgs created through signup/bootstrap start with `50` credits.
99
+ - Identity linking is email-based: later human sign-in with the same normalized email maps to the same user/org.
100
+
101
+ ## API endpoint -> CLI command map
102
+
103
+ | API endpoint | CLI command |
104
+ | --- | --- |
105
+ | `GET /api/capabilities` | `autotouch capabilities` |
106
+ | `POST /api/tables` | `autotouch tables create --name "My Table"` |
107
+ | `GET /api/tables?view_mode=org` | `autotouch tables list --view-mode org` |
108
+ | `POST /api/tables/{table_id}/rows` | `autotouch rows add --table-id <TABLE_ID> --records-file rows.json` |
109
+ | `POST /api/tables/{table_id}/import-optimized` | `autotouch rows import-csv --table-id <TABLE_ID> --file contacts.csv` |
110
+ | `POST /api/tables/{table_id}/csv-validate` | `autotouch rows import-csv --table-id <TABLE_ID> --file contacts.csv --validate-only` |
111
+ | `GET /api/tables/{table_id}/import-status/{task_id}` | `autotouch rows import-status --table-id <TABLE_ID> --task-id <TASK_ID>` |
112
+ | `GET /api/tables/{table_id}/import-verify/{task_id}` | `autotouch rows import-verify --table-id <TABLE_ID> --task-id <TASK_ID> --expected-rows <N>` |
113
+ | `POST /api/tables/{table_id}/import-rollback/{task_id}` | `autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID>` |
114
+ | `GET /api/blacklist` | `autotouch blacklist list --type-filter all --limit 100` |
115
+ | `POST /api/blacklist` | `autotouch blacklist add --type domain --value competitor.com` |
116
+ | `DELETE /api/blacklist/{entry_id}` | `autotouch blacklist remove --entry-id <ENTRY_ID>` |
117
+ | `POST /api/blacklist/import` | `autotouch blacklist import --file blacklist.csv` |
118
+ | `POST /api/blacklist/check` | `autotouch blacklist check --emails-file recipients.csv --emails-column email` |
119
+ | `POST /api/blacklist/filter` | `autotouch blacklist filter --emails-file recipients.csv --emails-column email` |
120
+ | `PATCH /api/tables/{table_id}/cells` | `autotouch cells patch --table-id <TABLE_ID> --updates-file updates.json` |
121
+ | `GET /api/tables/{table_id}/columns` | `autotouch columns list --table-id <TABLE_ID>` |
122
+ | `POST /api/tables/{table_id}/columns` | `autotouch columns create --table-id <TABLE_ID> --data-file column.json` |
123
+ | `PATCH /api/tables/{table_id}/columns/{column_id}` | `autotouch columns update --table-id <TABLE_ID> --column-id <COLUMN_ID> --data-file column-update.json` |
124
+ | `DELETE /api/tables/{table_id}/columns/{column_id}` | `autotouch columns delete --table-id <TABLE_ID> --column-id <COLUMN_ID> --yes` |
125
+ | `POST /api/tables/{table_id}/columns/projections` | `autotouch columns projections --table-id <TABLE_ID> --data-file projections.json` |
126
+ | `POST /api/tables/{table_id}/columns/{column_id}/estimate` | `autotouch columns estimate --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope all` |
127
+ | `POST /api/tables/{table_id}/columns/{column_id}/run` | `autotouch columns run --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope all` |
128
+ | `POST /api/tables/{table_id}/columns/{column_id}/stop` | `autotouch columns stop --table-id <TABLE_ID> --column-id <COLUMN_ID>` |
129
+ | `GET /api/bulk-jobs` | `autotouch jobs list --table-id <TABLE_ID> --column-id <COLUMN_ID> --limit 10` |
130
+ | `GET /api/bulk-jobs/{job_id}` | `autotouch jobs get --job-id <JOB_ID>` |
131
+ | `GET /api/tables/{table_id}/webhook` | `autotouch webhooks get --table-id <TABLE_ID>` |
132
+ | `POST /api/tables/{table_id}/webhook` | `autotouch webhooks rotate --table-id <TABLE_ID>` |
133
+ | `POST /api/webhooks/tables/{table_id}/ingest` | `autotouch webhooks ingest --table-id <TABLE_ID> --records-file records.json --webhook-token <WEBHOOK_TOKEN>` |
134
+ | `POST /api/auth/agent-bootstrap` | HTTP-only bootstrap (no direct CLI wrapper yet) |
135
+
136
+ ## Delete column
137
+
138
+ Delete is a hard delete:
139
+ - removes the column definition
140
+ - removes all cells stored under that column
141
+ - requires `columns:write`
142
+
143
+ CLI:
144
+
145
+ ```bash
146
+ autotouch columns delete --table-id <TABLE_ID> --column-id <COLUMN_ID> --yes
147
+ ```
148
+
149
+ ## Workflow API coverage (sequences/tasks)
150
+
151
+ Sequences/tasks APIs are supported by the backend developer-key model but do not yet have dedicated `autotouch` CLI commands.
152
+
153
+ Use raw HTTP for these endpoints today, with the same `stk_...` key:
154
+ - `POST /api/sequences`
155
+ - `PUT /api/sequences/{sequence_id}`
156
+ - `PATCH /api/sequences/{sequence_id}/status`
157
+ - `POST /api/sequences/{sequence_id}/enroll`
158
+ - `POST /api/task-queue/create`
159
+ - `PUT /api/task-queue/{task_id}`
160
+ - `POST /api/task-queue/{task_id}/draft`
161
+ - `POST /api/task-queue/{task_id}/email/schedule`
162
+
163
+ Reference contract (actor model + manual/automated/AI-draft nuances):
164
+ - `docs/platform/external-workflows-api.md`
165
+
166
+ Signature controls for sequence payloads:
167
+ - `defaultAppendSignature` (optional, sequence-level) sets the default signature-append behavior for email steps.
168
+ - `steps[].appendSignature` (optional, step-level) overrides signature behavior for a specific email step.
169
+ - Signature append is deterministic at send time when enabled for the step.
170
+
171
+ ## Bulk job status contract (authoritative run state)
172
+
173
+ Use bulk jobs as the source of truth for run lifecycle:
174
+
175
+ ```bash
176
+ autotouch jobs get --job-id <JOB_ID> --output json
177
+ ```
178
+
179
+ Important fields:
180
+ - `status`: `queued` | `distributing` | `processing` | `completed` | `partial` | `cancelled` | `error`
181
+ - `processed_rows`: successful rows
182
+ - `error_rows`: error rows
183
+ - `skipped_rows`: intentionally skipped/ineligible rows
184
+ - `total_rows`: scoped row count for the run
185
+ - `pending_batches`: derived remaining batches
186
+ - `terminal_reason`: terminal classifier
187
+
188
+ Terminal states:
189
+ - `completed`
190
+ - `partial`
191
+ - `cancelled`
192
+ - `error`
193
+
194
+ Agent rule:
195
+ - Do not infer completion from local process output alone.
196
+ - Use `jobs get` counters + terminal status as canonical truth.
197
+ - If `job_id` is missing from local logs, recover it with `jobs list` filtered by `table_id` + `column_id`.
198
+
199
+ ## Column create payload recipes (CLI-ready)
200
+
201
+ All examples below are used with:
202
+
203
+ ```bash
204
+ autotouch columns create --table-id <TABLE_ID> --data-file <payload.json>
205
+ ```
206
+
207
+ Recommendation:
208
+ - For provider-backed workflow columns, start with `autotouch columns recipe`:
209
+ `add_to_crm`, `sync_to_table`, `add_to_sequence`.
210
+ - Email/phone enrichment does not require creating `add_to_crm` first.
211
+ - `add_to_crm` is an optional, non-billable export action.
212
+
213
+ ### 1) `email_finder`
214
+
215
+ `email-finder.json`:
216
+
217
+ ```json
218
+ {
219
+ "key": "work_email",
220
+ "label": "Work Email",
221
+ "kind": "enrichment",
222
+ "dataType": "json",
223
+ "origin": "email_finder",
224
+ "autoRun": "never",
225
+ "config": {
226
+ "provider": "smart_email_finder",
227
+ "strategy": "cost_optimized",
228
+ "lookupStrategy": "linkedin",
229
+ "linkedinOnly": true,
230
+ "enableProfileFallback": false,
231
+ "linkedinUrl": "linkedin_url"
232
+ }
233
+ }
234
+ ```
235
+
236
+ ### 2) `phone_finder`
237
+
238
+ `phone-finder.json`:
239
+
240
+ ```json
241
+ {
242
+ "key": "mobile_phone",
243
+ "label": "Mobile Phone",
244
+ "kind": "enrichment",
245
+ "dataType": "json",
246
+ "origin": "phone_finder",
247
+ "autoRun": "never",
248
+ "config": {
249
+ "provider": "smart_phone_finder",
250
+ "firstName": "first_name",
251
+ "lastName": "last_name",
252
+ "company": "company",
253
+ "linkedinUrl": "linkedin_url"
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### 3) `lead_finder`
259
+
260
+ `lead-finder.json`:
261
+
262
+ ```json
263
+ {
264
+ "key": "lead_contacts",
265
+ "label": "Lead Contacts",
266
+ "kind": "enrichment",
267
+ "dataType": "json",
268
+ "origin": "ai",
269
+ "autoRun": "never",
270
+ "config": {
271
+ "provider": "lead_finder",
272
+ "sourceMode": "bulk_companies",
273
+ "companyDomain": "domain",
274
+ "strictness": "flexible_roles",
275
+ "storeAsLeads": true,
276
+ "autoEnrichEmails": true,
277
+ "autoEnrichPhones": false,
278
+ "jobTitles": ["Head of Sales", "VP Sales"],
279
+ "locations": ["United States"],
280
+ "maxResults": 10
281
+ }
282
+ }
283
+ ```
284
+
285
+ ### 4) `llm_enrichment`
286
+
287
+ `llm-enrichment.json`:
288
+
289
+ ```json
290
+ {
291
+ "key": "company_research",
292
+ "label": "Company Research",
293
+ "kind": "enrichment",
294
+ "dataType": "json",
295
+ "origin": "ai",
296
+ "autoRun": "never",
297
+ "config": {
298
+ "prompt": "Research this company and return JSON with icp_fit, summary, and risks.",
299
+ "mode": "agent",
300
+ "temperature": 0.7,
301
+ "batchSize": 50,
302
+ "promptSource": "manual",
303
+ "structuredOutput": true,
304
+ "useAutoSchema": true
305
+ }
306
+ }
307
+ ```
308
+
309
+ If you provide a custom schema, use strict Schema V2 field-map shape:
310
+
311
+ ```json
312
+ {
313
+ "key": "district_it_contact",
314
+ "label": "District IT Contact",
315
+ "kind": "enrichment",
316
+ "dataType": "json",
317
+ "origin": "ai",
318
+ "autoRun": "never",
319
+ "config": {
320
+ "provider": "xai",
321
+ "mode": "agent",
322
+ "structuredOutput": true,
323
+ "useAutoSchema": false,
324
+ "response_schema": {
325
+ "first_name": "string",
326
+ "last_name": "string",
327
+ "title": "string",
328
+ "email": "string",
329
+ "evidence_sources": {
330
+ "type": "array",
331
+ "items": {
332
+ "type": "object",
333
+ "properties": {
334
+ "type": "string",
335
+ "url": "string",
336
+ "snippet": "string"
337
+ }
338
+ }
339
+ },
340
+ "reasoning": "string"
341
+ },
342
+ "user_schema": {
343
+ "first_name": "string",
344
+ "last_name": "string",
345
+ "title": "string",
346
+ "email": "string",
347
+ "evidence_sources": {
348
+ "type": "array",
349
+ "items": {
350
+ "type": "object",
351
+ "properties": {
352
+ "type": "string",
353
+ "url": "string",
354
+ "snippet": "string"
355
+ }
356
+ }
357
+ },
358
+ "reasoning": "string"
359
+ }
360
+ }
361
+ }
362
+ ```
363
+
364
+ Schema guardrails:
365
+ - Root must be a field map (no root `{"type":"object","properties":...}` wrapper).
366
+ - Arrays must be explicit `{"type":"array","items":...}`.
367
+ - Never use list-literal schema values like `["string"]` or `[{...}]`.
368
+ - Prefer snake_case schema keys (`response_schema`, `user_schema`, `use_auto_schema`); camelCase variants are accepted in compatibility paths.
369
+
370
+ ### 5) `formatter`
371
+
372
+ `formatter.json`:
373
+
374
+ ```json
375
+ {
376
+ "key": "engagement_statement",
377
+ "label": "Engagement Statement",
378
+ "kind": "formatter",
379
+ "dataType": "text",
380
+ "origin": "manual",
381
+ "autoRun": "onSourceUpdate",
382
+ "config": {
383
+ "formula": "return (row['first_name'] || '') + ' ' + (row['last_name'] || '') + \" reacted to David Wilkins' post: \\\"\" + (row['post_content'] || '') + \"\\\"\";",
384
+ "sourceColumns": ["first_name", "last_name", "post_content"]
385
+ }
386
+ }
387
+ ```
388
+
389
+ Notes:
390
+ - Formatter formulas must reference `row` keys (`row['first_name']`, `row.last_name`).
391
+ - Bare template placeholders like ``${first_name}`` are invalid and rejected.
392
+ - Formatter columns are source-driven; backend policy normalizes formatter `autoRun` to `onSourceUpdate`.
393
+
394
+ ### 6) `add_to_crm`
395
+
396
+ `add-to-crm.json`:
397
+
398
+ ```json
399
+ {
400
+ "key": "add_to_leads",
401
+ "label": "Add to Leads",
402
+ "kind": "enrichment",
403
+ "dataType": "json",
404
+ "origin": "manual",
405
+ "autoRun": "onSourceUpdate",
406
+ "config": {
407
+ "provider": "add_to_crm",
408
+ "leadSource": "research_table_export",
409
+ "fieldMappings": {
410
+ "mode": "single",
411
+ "linkedinUrl": "linkedin_url",
412
+ "companyDomain": "domain",
413
+ "firstName": "first_name",
414
+ "lastName": "last_name",
415
+ "title": "title",
416
+ "companyName": "company",
417
+ "emailAddresses": [
418
+ { "column": "work_email", "type": "work" }
419
+ ],
420
+ "phoneNumbers": [
421
+ { "column": "mobile_phone", "type": "mobile" }
422
+ ]
423
+ },
424
+ "sourceColumns": [
425
+ "linkedin_url",
426
+ "domain",
427
+ "first_name",
428
+ "last_name",
429
+ "title",
430
+ "company",
431
+ "work_email",
432
+ "mobile_phone"
433
+ ]
434
+ }
435
+ }
436
+ ```
437
+
438
+ Add-to-Leads note: mapped LinkedIn URL + company domain are required; run is non-billable.
439
+ If `companyDomain` is missing in the table, derive or enrich that domain column first, then rerun `add_to_crm`.
440
+
441
+ CRM data model expectations (recommended before `add_to_crm`):
442
+ - Lead identity/dedupe expects `linkedin_url` + `company_domain` (clean domain like `example.com`).
443
+ - `company_domain` is required; `company_name` is only a display hint and is applied to the linked Company record when provided.
444
+ - Lead records link to Company via `company_id`; company names live on Company docs, not as canonical lead fields.
445
+ - 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.
446
+ - Reference docs:
447
+ - `docs/data/leads.md`
448
+ - `docs/data/companies.md`
449
+
450
+ ### 7) `sync_to_table`
451
+
452
+ `sync-to-table.json`:
453
+
454
+ ```json
455
+ {
456
+ "key": "sync_to_table",
457
+ "label": "Sync to Table",
458
+ "kind": "enrichment",
459
+ "dataType": "json",
460
+ "origin": "manual",
461
+ "autoRun": "onSourceUpdate",
462
+ "config": {
463
+ "provider": "sync_to_table",
464
+ "destinationTableId": "<DESTINATION_TABLE_ID>",
465
+ "columnMappings": [
466
+ { "sourceKey": "company", "destKey": "company" },
467
+ { "sourceKey": "domain", "destKey": "company_domain" },
468
+ { "sourceKey": "work_email", "destKey": "work_email" }
469
+ ]
470
+ }
471
+ }
472
+ ```
473
+
474
+ Notes:
475
+ - Single-destination mode uses `destinationTableId` + `columnMappings`.
476
+ - Router mode is also supported with `config.routes[]` (top-to-bottom matching, first hit wins, default route catches unmatched rows).
477
+
478
+ ### 8) `add_to_sequence`
479
+
480
+ `add-to-sequence.json`:
481
+
482
+ ```json
483
+ {
484
+ "key": "add_to_sequence",
485
+ "label": "Add to Sequence",
486
+ "kind": "enrichment",
487
+ "dataType": "json",
488
+ "origin": "manual",
489
+ "autoRun": "onSourceUpdate",
490
+ "config": {
491
+ "provider": "add_to_sequence",
492
+ "sequenceId": "<SEQUENCE_ID>",
493
+ "sourceLeadColumn": "add_to_leads"
494
+ }
495
+ }
496
+ ```
497
+
498
+ Notes:
499
+ - `sourceLeadColumn` must point to a column that stores lead IDs (for example `add_to_crm` or `lead_finder` output).
500
+ - `sequenceId` is the target sequence workflow ID.
501
+ - 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.
502
+ - `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.
503
+ - Star/favorite the highest-signal fields so callers can see them quickly in the sidecar during live call workflows.
504
+ - The same favorite set is also the default AI drafting context when explicit `fieldIds` are not provided.
505
+
506
+ ## Common workflow
507
+
508
+ ```bash
509
+ autotouch capabilities
510
+ autotouch tables create --name "CLI Contacts"
511
+ autotouch rows import-csv --table-id <TABLE_ID> --file contacts.csv
512
+ autotouch columns recipe --type add_to_crm --out-file column.json
513
+ autotouch columns create --table-id <TABLE_ID> --data-file column.json
514
+ autotouch columns recipe --type add_to_sequence --out-file add-to-sequence.json
515
+ autotouch columns create --table-id <TABLE_ID> --data-file add-to-sequence.json
516
+ autotouch columns run-next --table-id <TABLE_ID> --column-id <COLUMN_ID> --count 25 --filters-file filters.json --show-estimate --wait
517
+ autotouch jobs get --job-id <JOB_ID>
518
+ ```
519
+
520
+ ## Safe run patterns (`firstN` + `--unprocessed-only`)
521
+
522
+ Use this pattern for progressive rollouts.
523
+
524
+ ```bash
525
+ # Pilot first 10 rows
526
+ autotouch columns run \
527
+ --table-id <TABLE_ID> \
528
+ --column-id <COLUMN_ID> \
529
+ --scope firstN \
530
+ --first-n 10 \
531
+ --unprocessed-only \
532
+ --show-estimate \
533
+ --wait
534
+
535
+ # Extend to first 15 rows (processes the next 5 if first 10 are already done)
536
+ autotouch columns run \
537
+ --table-id <TABLE_ID> \
538
+ --column-id <COLUMN_ID> \
539
+ --scope firstN \
540
+ --first-n 15 \
541
+ --unprocessed-only \
542
+ --show-estimate \
543
+ --wait
544
+ ```
545
+
546
+ Notes:
547
+ - `firstN` without `--unprocessed-only` can re-run already-processed rows.
548
+ - With `--unprocessed-only`, `firstN` means "first N currently eligible unprocessed rows", not "exactly N new rows since your last check".
549
+ - If you need an exact count (for example exactly 5 rows), use `run-next` below.
550
+ - 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.
551
+ - `--wait` polls `/api/bulk-jobs/{job_id}` until terminal status.
552
+ - If a job stays `queued`, workers for that provider queue may be scaled to `0`.
553
+ - During execution, non-final batches remain `processing`; they should not be treated as complete.
554
+
555
+ ## Exact count runs (`run-next`)
556
+
557
+ Use this when you need exactly `N` rows in one run.
558
+ The CLI selects candidate row IDs first, then executes `/run` with `scope=subset`.
559
+
560
+ ```bash
561
+ # Run exactly 5 unprocessed rows from the current view
562
+ autotouch columns run-next \
563
+ --table-id <TABLE_ID> \
564
+ --column-id <COLUMN_ID> \
565
+ --count 5 \
566
+ --filters-file filters.json \
567
+ --show-estimate \
568
+ --wait
569
+ ```
570
+
571
+ Notes:
572
+ - Default behavior is unprocessed-only selection.
573
+ - Add `--include-processed` to allow already-processed rows into candidate selection.
574
+ - `run-next` is deterministic on count (subject to available eligible rows).
575
+ - If fewer than `N` eligible rows exist, it runs the available subset and reports selected count.
576
+
577
+ ### Agent execution contract (strict)
578
+
579
+ When operating this CLI as an agent, use backend job state as source of truth:
580
+
581
+ 1. Treat a run as started only if `/run` returns a `jobId` (`job_id`).
582
+ 2. Treat a run as completed only when `GET /api/bulk-jobs/{job_id}` returns terminal status.
583
+ 3. Never infer progress/completion from local process liveness alone.
584
+ 4. If polling is blocked by local network/approval/sandbox constraints, report "run state not confirmed" (do not claim still running/completed).
585
+ 5. If polling returns `not_found` or `unknown_not_found`, treat that run as failed/ambiguous and verify row state before retry.
586
+
587
+ Agent output contract:
588
+ - Prefer `--output json` (and `--compact` when token budget matters).
589
+ - Parse machine fields only (`job_id`, `status`, `processed_rows`, `error_rows`, `skipped_rows`, `total_rows`).
590
+ - Do not infer success from human-readable log lines.
591
+ - If response parsing fails, treat run state as unknown and recover via `autotouch jobs list` + `autotouch jobs get`.
592
+
593
+ ### Enrichment value parsing contract (phone + email)
594
+
595
+ When summarizing enrichment results, use this sequence:
596
+
597
+ 1. Inspect raw outputs first (at least 3 sample rows).
598
+ 2. Choose parser mode from column `dataType`.
599
+ 3. For `dataType=json`, apply key precedence below.
600
+ 4. For scalar types (`text`, `number`, `date`, `boolean`, `email`, `url`), read direct scalar values (no JSON key-path parsing).
601
+
602
+ For JSON outputs, parse value payloads by precedence instead of a single key.
603
+
604
+ Phone value extraction order:
605
+
606
+ 1. `mobile_number`
607
+ 2. `phone_numbers[0].number`
608
+ 3. `primary_phone`
609
+
610
+ Email value extraction order:
611
+
612
+ 1. `response`
613
+ 2. `email`
614
+ 3. `work_email`
615
+
616
+ Important:
617
+ - Do not treat missing `response`/`phone` as a hard miss for phone finder.
618
+ - If top-level path is missing, continue to fallback paths before reporting `not_found`.
619
+ - Validate the parser against a few raw sample rows before publishing counts.
620
+
621
+ JSON split note:
622
+ - `columns projections` is optional by default.
623
+ - Use it when downstream filtering/mapping/sequence variable binding needs stable flat keys.
624
+ - Creating the projection is enough for existing rows; the backend backfills/materializes those values automatically.
625
+ - If source columns are JSON enrichments (email/phone/LLM), run the source column first with `--wait` and confirm terminal job status before splitting.
626
+ - CLI behavior: `columns projections` will emit preflight warnings when a JSON enrichment source appears unrun/unverified.
627
+ - Warning output contract: when warnings exist, JSON output is wrapped as `{ "event": "projections.created_with_warnings", "warnings": [...], "result": <api_response> }`.
628
+
629
+ Reference playbook + runbook:
630
+ - `docs/research-table/guides/context-first-sequence-playbook.md`
631
+ - `docs/research-table/reference/runbooks/context-first-sequence.json`
632
+
633
+ ### Wait output contract (CLI >= 0.2.11)
634
+
635
+ `columns run --wait` and `columns run-next --wait` now emit structured lifecycle events:
636
+
637
+ - `run.wait_started`
638
+ - `job.progress`
639
+ - `run.completed` / `run.timed_out`
640
+
641
+ Run outputs include:
642
+
643
+ - `job_id`
644
+ - `job_status_url`
645
+ - `watch_command` (copy-paste fallback for explicit polling)
646
+
647
+ Terminal status values:
648
+
649
+ - `completed`
650
+ - `partial`
651
+ - `error`
652
+ - `cancelled`
653
+
654
+ CLI-protected failure statuses:
655
+
656
+ - `not_found`
657
+ - `unknown_not_found`
658
+
659
+ Non-terminal status values:
660
+
661
+ - `queued`
662
+ - `distributing`
663
+ - `processing`
664
+
665
+ Recommended fields to read from `jobs get` while waiting:
666
+ - `processed_rows`
667
+ - `error_rows`
668
+ - `skipped_rows`
669
+ - `total_rows`
670
+ - `pending_batches`
671
+ - `terminal_reason`
672
+
673
+ ### Canonical fallback (when `--wait` is noisy in your runtime)
674
+
675
+ ```bash
676
+ # 1) Queue run and capture jobId
677
+ autotouch columns run \
678
+ --table-id <TABLE_ID> \
679
+ --column-id <COLUMN_ID> \
680
+ --scope firstN \
681
+ --first-n 15 \
682
+ --unprocessed-only \
683
+ --show-estimate \
684
+ --output json
685
+
686
+ # 2) If jobId was not captured, recover latest from backend history
687
+ autotouch jobs list \
688
+ --table-id <TABLE_ID> \
689
+ --column-id <COLUMN_ID> \
690
+ --limit 1 \
691
+ --output json
692
+
693
+ # 3) Poll backend truth directly
694
+ autotouch jobs get --job-id <JOB_ID> --output json
695
+ ```
696
+
697
+ Repeat `jobs get` until status is terminal.
698
+
699
+ ## CSV import (agent-safe, async-first)
700
+
701
+ `rows import-csv` defaults to optimized import transport (`/import-optimized`) so large files do not fail on a single long request.
702
+
703
+ ```bash
704
+ # Queue background import and return task_id quickly
705
+ autotouch rows import-csv \
706
+ --table-id <TABLE_ID> \
707
+ --confirm-table-id <TABLE_ID> \
708
+ --file contacts.csv \
709
+ --checkpoint-file .autotouch-import.json
710
+
711
+ # Wait for completion
712
+ autotouch rows import-status \
713
+ --table-id <TABLE_ID> \
714
+ --task-id <TASK_ID> \
715
+ --wait
716
+ ```
717
+
718
+ Notes:
719
+ - Use `--sync` only when you explicitly want synchronous behavior.
720
+ - Legacy direct path is still available with `--transport direct`.
721
+ - Optimized import emits progressive events while processing, and starts with a small first batch for fast initial row visibility.
722
+ - Use `--dry-run` for parse-only preview before writing rows.
723
+ - Use `--validate-only` to test server-side CSV parsing/shape without writing rows.
724
+ - Blacklist controls on optimized import:
725
+ - company-domain filtering is ON by default
726
+ - email filtering is OFF by default
727
+ - disable company filtering with `--no-check-company-blacklist`
728
+ - enable email filtering with `--check-email-blacklist`
729
+ - Safety assertions are available on import:
730
+ - `--expected-rows <N>`
731
+ - `--require-columns col_a,col_b`
732
+ - `--duplicate-key col_a,col_b`
733
+ - `--require-non-empty post_content:0.95`
734
+ - Assertions run as preflight checks. For async imports with `--wait`, postflight verification runs against the persisted task manifest automatically.
735
+ - Use `--allow-reimport` only when intentionally importing the same file again.
736
+ - Import responses include `blacklist_summary` with company/email `skipped_count` and `enforced` flags.
737
+
738
+ Safe protocol:
739
+
740
+ ```bash
741
+ # 1) Validate parse/shape only (no writes)
742
+ autotouch rows import-csv --table-id <TABLE_ID> --confirm-table-id <TABLE_ID> --file contacts.csv --validate-only
743
+
744
+ # 2) Import with strict assertions + wait
745
+ autotouch rows import-csv \
746
+ --table-id <TABLE_ID> \
747
+ --confirm-table-id <TABLE_ID> \
748
+ --file contacts.csv \
749
+ --checkpoint-file .autotouch-import.json \
750
+ --check-email-blacklist \
751
+ --expected-rows 57 \
752
+ --require-columns first_name,last_name,post_content \
753
+ --duplicate-key linkedin_url,post_url \
754
+ --require-non-empty post_content:1 \
755
+ --wait
756
+
757
+ # 3) Re-verify later (optional)
758
+ autotouch rows import-verify --table-id <TABLE_ID> --task-id <TASK_ID> --expected-rows 57
759
+
760
+ # 4) Roll back by task_id if needed
761
+ autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID>
762
+ ```
763
+
764
+ Manage blacklist entries (native CLI, admin identity required):
765
+
766
+ ```bash
767
+ # List current entries
768
+ autotouch blacklist list --type-filter all --limit 100
769
+
770
+ # Add entries
771
+ autotouch blacklist add --type domain --value competitor.com --reason "Do not contact"
772
+ autotouch blacklist add --type email --value blocked@example.com --reason "Unsubscribed"
773
+
774
+ # Bulk-import entries from CSV/TXT
775
+ autotouch blacklist import --file blacklist.csv
776
+
777
+ # Check or filter email sets (auto-chunked for large lists)
778
+ autotouch blacklist check --emails-file recipients.csv --emails-column email --summary-only
779
+ autotouch blacklist filter --emails-file recipients.csv --emails-column email --summary-only
780
+ ```
781
+
782
+ Recommended timing (cost + ICP guardrail):
783
+ - Add known suppressions early (unsubscribed addresses, do-not-contact domains, existing customers, competitors, and clear non-ICP targets).
784
+ - Before billable enrichments (`llm_enrichment`, `email_finder`, `phone_finder`), run blacklist filtering on candidate emails and enrich only clean rows.
785
+ - Run a final blacklist check/filter again before downstream outreach or dialing.
786
+
787
+ ```bash
788
+ # Example: pre-enrichment blacklist gate
789
+ autotouch blacklist filter \
790
+ --emails-file candidates.csv \
791
+ --emails-column work_email \
792
+ --output json
793
+ ```
794
+
795
+ ## Capabilities for agents
796
+
797
+ Use capabilities as the source of truth before generating payloads:
798
+
799
+ ```bash
800
+ autotouch capabilities
801
+ ```
802
+
803
+ Agent expectations:
804
+
805
+ - `column_types` tells you which column types are runnable and which are non-billable transforms (`json_split`, `formatter_formula`).
806
+ - `filtering` describes valid scope/filter semantics for estimate/run.
807
+ - `automation.auto_run` describes supported auto-run modes + config field names.
808
+ - `execution_policies.llm.output_contract` describes output behavior by mode:
809
+ - `agent` => JSON-oriented structured output
810
+ - `basic` => text or JSON (`dataType=json` for structured JSON output)
811
+ - `webhooks.table_ingest` describes webhook ingest auth contract (metadata only; no secret tokens).
812
+
813
+ For a built-in machine-readable run playbook, use:
814
+
815
+ ```bash
816
+ autotouch sop --output json
817
+ ```
818
+
819
+ ## JSON output pipeline pattern
820
+
821
+ For enrichment responses that return structured JSON, use this chain:
822
+
823
+ 1. Run enrichment into a JSON column.
824
+ 2. Wait for terminal status (`completed`/`partial`/`error`/`cancelled`) using `--wait` or `jobs get/watch`.
825
+ 3. Split JSON into projection columns (optional; only when stable flat keys are needed).
826
+ 4. Optional formatter normalization.
827
+ 5. Feed extracted/normalized keys into downstream enrichment columns.
828
+
829
+ Important mode distinction:
830
+
831
+ - Agent mode is JSON-oriented and is intended for structured outputs.
832
+ - Basic mode can return plain text or JSON depending on your column `dataType`/schema setup.
833
+
834
+ ### Recommended ICP buyer pattern (agent mode)
835
+
836
+ For go-to-market workflows, prefer one best-fit buyer per row in this stage.
837
+
838
+ - Ask for exactly one person (not a list/array) with a flat JSON object.
839
+ - Recommended keys: `first_name`, `last_name`, `title`, `company_name`, `company_website`, `linkedin_url`.
840
+ - Then split those keys into projection columns and run email/phone enrichment on those outputs.
841
+ - Use `lead_finder` first for larger companies when role ownership is clear; use agent research for hard-to-find or low-coverage cases.
842
+ - Target responsibilities, not exact titles (for example: "most likely responsible for buying social/cell engagement software").
843
+
844
+ Prompt shape recommendation:
845
+
846
+ ```text
847
+ Find the single most likely buyer of cell engagement software for this company.
848
+ Return exactly one JSON object with keys:
849
+ first_name, last_name, title, company_name, company_website, linkedin_url.
850
+ Use empty string when unknown.
851
+ ```
852
+
853
+ Full strategy and examples:
854
+
855
+ - `docs/research-table/guides/icp-buyer-discovery.md`
856
+
857
+ ```bash
858
+ # Create projections from a JSON source column
859
+ autotouch columns projections \
860
+ --table-id <TABLE_ID> \
861
+ --data-file projections.json
862
+
863
+ # Optional: update downstream column config to reference projected keys
864
+ autotouch columns update \
865
+ --table-id <TABLE_ID> \
866
+ --column-id <DOWNSTREAM_COLUMN_ID> \
867
+ --data-file column-update.json
868
+ ```
869
+
870
+ `projections.json`:
871
+
872
+ ```json
873
+ {
874
+ "items": [
875
+ {
876
+ "key": "person_name",
877
+ "label": "Person Name",
878
+ "sourceColumnId": "<JSON_COLUMN_ID>",
879
+ "path": "person_name",
880
+ "dataType": "text"
881
+ },
882
+ {
883
+ "key": "school_domain",
884
+ "label": "School Domain",
885
+ "sourceColumnId": "<JSON_COLUMN_ID>",
886
+ "path": "school_domain",
887
+ "dataType": "text"
888
+ }
889
+ ]
890
+ }
891
+ ```
892
+
893
+ ### First principles: intent + context
894
+
895
+ For most workflows, useful output has two layers:
896
+ - context: source evidence (what happened / what was observed)
897
+ - intent: interpretation of that evidence (what to prioritize / do next)
898
+
899
+ Design guidance:
900
+ - preserve full-fidelity context in at least one raw field (for example full post/body text)
901
+ - default behavior for agents: write full context/content, not summaries or truncation
902
+ - only truncate/summarize when there is a hard limit (storage/model/provider/payload), and mark that it was truncated
903
+ - keep intent separate from raw context so automation can evolve without data loss
904
+ - for calling workflows, star/favorite high-signal fields so sidecar context is immediately useful without extra clicks
905
+ - for AI-generated emails/copy, keep intent + full context together in imports so drafts are grounded in source evidence
906
+ - keep one entity per row and dedupe using a stable identity key
907
+ - treat snippets/summaries as optional derived fields, never the source of truth
908
+ - if automation depends on intent values, use a small normalized label taxonomy
909
+
910
+ Optional field pattern (adapt as needed):
911
+ - `post_content` or `context_raw`: full long-form context text
912
+ - `context_url`: source URL
913
+ - `context_timestamp`: recency marker
914
+ - `intent_label`: normalized intent category
915
+ - `intent_reason`: human-readable explanation
916
+
917
+ CSV handling note:
918
+ - long context fields may include newlines/commas and are valid when properly quoted
919
+ - run `autotouch rows import-csv --validate-only` (or `--dry-run`) first
920
+ - for strict guardrails, add assertions: `--expected-rows`, `--require-columns`, `--duplicate-key`, `--require-non-empty`
921
+ - for mutating imports, use `--confirm-table-id` and `--checkpoint-file` to reduce accidental corruption/duplicates
922
+ - import does not intentionally truncate text values; practical limits are the underlying MongoDB document-size limits
923
+
924
+ Multiline `post_content` examples:
925
+
926
+ ```csv
927
+ # bad (unquoted multiline content breaks row shape)
928
+ first_name,linkedin_url,post_content
929
+ Ada,https://linkedin.com/in/ada,Line 1
930
+ Line 2
931
+ ```
932
+
933
+ ```csv
934
+ # good (quoted multiline content is valid CSV)
935
+ first_name,linkedin_url,post_content
936
+ Ada,https://linkedin.com/in/ada,"Line 1
937
+ Line 2"
938
+ ```
939
+
940
+ Do not re-import blind (recovery flow):
941
+ - stop and keep the original `task_id`
942
+ - inspect status: `autotouch rows import-status --table-id <TABLE_ID> --task-id <TASK_ID>`
943
+ - prove postflight: `autotouch rows import-verify --table-id <TABLE_ID> --task-id <TASK_ID> ...assertions...`
944
+ - if verification fails, preview rollback: `autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID> --dry-run`
945
+ - then rollback: `autotouch rows import-rollback --table-id <TABLE_ID> --task-id <TASK_ID>`
946
+ - fix CSV quoting/mapping and run `--validate-only` before any new import
947
+
948
+ ## Filtering (credit control)
949
+
950
+ Use `scope=filtered` to run only matching rows.
951
+
952
+ `filters.json`:
953
+
954
+ ```json
955
+ {
956
+ "mode": "and",
957
+ "filters": [
958
+ { "columnKey": "linkedin_url", "operator": "isNotEmpty" },
959
+ { "columnKey": "country", "operator": "equals", "value": "United States" }
960
+ ]
961
+ }
962
+ ```
963
+
964
+ ```bash
965
+ # Estimate first (non-billable)
966
+ autotouch columns estimate \
967
+ --table-id <TABLE_ID> \
968
+ --column-id <COLUMN_ID> \
969
+ --scope filtered \
970
+ --filters-file filters.json \
971
+ --unprocessed-only
972
+
973
+ # Run same payload with rollout cap
974
+ autotouch columns run \
975
+ --table-id <TABLE_ID> \
976
+ --column-id <COLUMN_ID> \
977
+ --scope filtered \
978
+ --filters-file filters.json \
979
+ --unprocessed-only \
980
+ --first-n 200 \
981
+ --show-estimate --wait
982
+ ```
983
+
984
+ ### Cost tip: filter out empty rows between enrichments
985
+
986
+ Most teams run paid enrichments only on rows that already have required upstream data.
987
+ This avoids spending credits on rows that cannot produce useful results yet.
988
+
989
+ Example: run email finder only when `linkedin_url` exists and `work_email_address` is still empty.
990
+
991
+ ```json
992
+ {
993
+ "mode": "and",
994
+ "filters": [
995
+ { "columnKey": "linkedin_url", "operator": "isNotEmpty" },
996
+ { "columnKey": "work_email_address", "operator": "isEmpty" }
997
+ ]
998
+ }
999
+ ```
1000
+
1001
+ Pattern to reuse:
1002
+ - Step 1: create/select a filter that excludes empty prerequisite fields.
1003
+ - Step 2: run small (`firstN` or `run-next`) with `--show-estimate`.
1004
+ - Step 3: expand only after output quality looks good.
1005
+
1006
+ ### Cost tip: run blacklist gate before billable enrichments
1007
+
1008
+ 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).
1009
+
1010
+ ```bash
1011
+ autotouch blacklist filter \
1012
+ --emails-file candidates.csv \
1013
+ --emails-column work_email \
1014
+ --output json
1015
+ ```
1016
+
1017
+ ## Auto-run configuration
1018
+
1019
+ Auto-run is set on the column definition (`autoRun`) and can be changed later with `columns update`.
1020
+
1021
+ Formatter-specific rule:
1022
+ - Formatter columns are always normalized to `autoRun: "onSourceUpdate"` by the backend.
1023
+ - Attempting to set formatter `autoRun` to `never` or `onInsert` is ignored/overridden server-side.
1024
+
1025
+ `column-update.json`:
1026
+
1027
+ ```json
1028
+ {
1029
+ "autoRun": "onInsert",
1030
+ "config": {
1031
+ "autoRunMode": "conditional",
1032
+ "autoRunFilters": {
1033
+ "mode": "and",
1034
+ "filters": [
1035
+ { "columnKey": "linkedin_url", "operator": "isNotEmpty" }
1036
+ ]
1037
+ }
1038
+ }
1039
+ }
1040
+ ```
1041
+
1042
+ ```bash
1043
+ autotouch columns update \
1044
+ --table-id <TABLE_ID> \
1045
+ --column-id <COLUMN_ID> \
1046
+ --data-file column-update.json
1047
+ ```
1048
+
1049
+ ## Table webhooks (ingest)
1050
+
1051
+ Webhook ingestion uses a per-table token, not developer API key scopes.
1052
+
1053
+ ```bash
1054
+ # Read current webhook config
1055
+ autotouch webhooks get --table-id <TABLE_ID>
1056
+
1057
+ # Create/rotate token (token is returned once)
1058
+ autotouch webhooks rotate --table-id <TABLE_ID>
1059
+ ```
1060
+
1061
+ `records.json`:
1062
+
1063
+ ```json
1064
+ {
1065
+ "records": [
1066
+ { "first_name": "Ada", "email": "ada@example.com" }
1067
+ ]
1068
+ }
1069
+ ```
1070
+
1071
+ ```bash
1072
+ # Send records with webhook token
1073
+ autotouch webhooks ingest \
1074
+ --table-id <TABLE_ID> \
1075
+ --records-file records.json \
1076
+ --webhook-token <WEBHOOK_TOKEN>
1077
+ ```
1078
+
1079
+ ## Outbound webhook subscriptions
1080
+
1081
+ Use outbound subscriptions to receive business events (`bulk_job.*`, `lead.*`, `sequence_enrollment.created`, `task.created`).
1082
+
1083
+ ```bash
1084
+ # List subscriptions
1085
+ autotouch webhooks subscriptions list
1086
+
1087
+ # Create subscription
1088
+ autotouch webhooks subscriptions create \
1089
+ --url https://example.com/webhooks/smart-table \
1090
+ --events bulk_job.* lead.status_changed task.created
1091
+
1092
+ # Pause/resume
1093
+ autotouch webhooks subscriptions pause --subscription-id <SUBSCRIPTION_ID>
1094
+ autotouch webhooks subscriptions resume --subscription-id <SUBSCRIPTION_ID>
1095
+
1096
+ # Rotate signing secret
1097
+ autotouch webhooks subscriptions rotate-secret --subscription-id <SUBSCRIPTION_ID>
1098
+
1099
+ # Fire test event
1100
+ autotouch webhooks subscriptions test \
1101
+ --subscription-id <SUBSCRIPTION_ID> \
1102
+ --event-type lead.created \
1103
+ --data-json '{"ping":"ok"}'
1104
+
1105
+ # Inspect deliveries + attempts
1106
+ autotouch webhooks deliveries list --subscription-id <SUBSCRIPTION_ID> --limit 50
1107
+ autotouch webhooks deliveries attempts --delivery-id <DELIVERY_ID>
1108
+ ```
1109
+
1110
+ Required scopes for developer API keys:
1111
+ - `webhooks:read` (list/get/deliveries)
1112
+ - `webhooks:write` (create/update/delete/pause/resume/rotate/test)
1113
+
1114
+ Retention note:
1115
+ - Webhook event cache and delivery-attempt logs default to 7 days
1116
+ (`WEBHOOK_EVENTS_TTL_DAYS`, `WEBHOOK_DELIVERY_ATTEMPTS_TTL_DAYS`).
1117
+
1118
+ ## Budget and safety controls
1119
+
1120
+ ```bash
1121
+ # Estimate only (no execution)
1122
+ autotouch columns run --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope filtered --filters-file filters.json --unprocessed-only --dry-run
1123
+
1124
+ # Guard against overspend
1125
+ autotouch columns run --table-id <TABLE_ID> --column-id <COLUMN_ID> --scope filtered --filters-file filters.json --unprocessed-only --max-credits 50
1126
+
1127
+ # Poll until terminal status
1128
+ autotouch jobs watch --job-id <JOB_ID>
1129
+
1130
+ # Stop a running column
1131
+ autotouch columns stop --table-id <TABLE_ID> --column-id <COLUMN_ID>
1132
+ ```
1133
+
1134
+ ## Notes
1135
+
1136
+ - The CLI is a thin wrapper around the same HTTP endpoints documented in `docs/research-table/reference/tables-api.md`.
1137
+ - If a key is missing scope, CLI commands fail the same way raw API calls do (`403`).
1138
+ - Use `--base-url` and `--token` per command for CI/ephemeral environments.