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