@alvera-ai/platform-sdk 0.10.0-rc.8 → 0.11.0

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 (66) hide show
  1. package/.agent/AGENTS.md +499 -0
  2. package/.agent/account_management.md +456 -0
  3. package/.agent/action_status_updaters.md +264 -0
  4. package/.agent/ai_agents.md +462 -0
  5. package/.agent/ai_sandbox.md +265 -0
  6. package/.agent/async.md +112 -0
  7. package/.agent/connected_apps.md +408 -0
  8. package/.agent/cookbook/_fixtures/README.md +106 -0
  9. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
  10. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
  11. package/.agent/cookbook/_fixtures/accounts_receivable/stripe_customers_batch1.csv +5 -0
  12. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
  13. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
  14. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
  15. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
  16. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
  17. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
  18. package/.agent/cookbook/_fixtures/healthcare/memorandum-of-association-01.png +0 -0
  19. package/.agent/cookbook/_fixtures/healthcare/sample_two_page.pdf +43 -0
  20. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
  21. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
  22. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
  23. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
  24. package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
  25. package/.agent/cookbook/_setup/foundation.md +277 -0
  26. package/.agent/cookbook/_setup/healthcare.md +279 -0
  27. package/.agent/cookbook/_setup/payment_risk.md +283 -0
  28. package/.agent/cookbook/action-status-updaters.md +212 -0
  29. package/.agent/cookbook/ai-agent-invoke.md +243 -0
  30. package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
  31. package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
  32. package/.agent/cookbook/bulk-ingest.md +254 -0
  33. package/.agent/cookbook/contact-us-triage-with-llm.md +622 -0
  34. package/.agent/cookbook/custom-tables.md +201 -0
  35. package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
  36. package/.agent/cookbook/invite-team.md +194 -0
  37. package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
  38. package/.agent/cookbook/rest-fetch.md +246 -0
  39. package/.agent/cookbook/sanctions-screening-with-agent-review.md +733 -0
  40. package/.agent/cookbook/score-leads-with-llm-categorization.md +624 -0
  41. package/.agent/cookbook/system-templates.md +129 -0
  42. package/.agent/cookbook/triage-prospects-by-priority.md +533 -0
  43. package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
  44. package/.agent/data_activation_clients.md +559 -0
  45. package/.agent/data_sources.md +235 -0
  46. package/.agent/datalakes.md +714 -0
  47. package/.agent/debugging.md +137 -0
  48. package/.agent/errors.md +190 -0
  49. package/.agent/generic_tables.md +351 -0
  50. package/.agent/interoperability_contracts.md +417 -0
  51. package/.agent/mdm.md +293 -0
  52. package/.agent/mutations.md +126 -0
  53. package/.agent/templates.md +98 -0
  54. package/.agent/tool-call-configs.md +90 -0
  55. package/.agent/tools.md +547 -0
  56. package/.agent/type_naming.md +129 -0
  57. package/.agent/workflows.md +617 -0
  58. package/README.md +178 -0
  59. package/dist/bin/platform-sdk.d.mts +1 -0
  60. package/dist/bin/platform-sdk.mjs +106 -0
  61. package/dist/bin/platform-sdk.mjs.map +1 -0
  62. package/dist/index.d.mts +1310 -44116
  63. package/dist/index.d.mts.map +1 -1
  64. package/dist/index.mjs +1194 -7344
  65. package/dist/index.mjs.map +1 -1
  66. package/package.json +19 -10
@@ -0,0 +1,559 @@
1
+ # Data activation clients
2
+
3
+ A **data activation client** is the pipeline join-point that
4
+ binds a **tool** (the transport — see `tools.md`), a **data
5
+ source** (the endpoint — see `data_sources.md`), and one or
6
+ more **interoperability contracts** (the row-to-resource
7
+ transforms — see `interoperability_contracts.md`) into a single
8
+ runnable ingestion pipeline.
9
+
10
+ SDK namespace: `api.dataActivationClients`. The same namespace
11
+ carries both the management surface (CRUD on the binding row,
12
+ §§1–5) and the runtime surface (the verbs that actually move
13
+ data through the configured pipeline, §6). There is no separate
14
+ "data activation runs" resource — runs are verbs on this
15
+ namespace, not a sibling entity.
16
+
17
+ Data activation clients are **Datalake-DB-resident** — the
18
+ parent datalake must be `status: 'ready'` and all referenced
19
+ resources (tool, data source, contracts) must exist before
20
+ you `create()`.
21
+
22
+ ## 1. Wire shape
23
+
24
+ ```typescript
25
+ import type {
26
+ DataActivationClientRequestWritable,
27
+ DataActivationClientResponse,
28
+ } from '@alvera-ai/platform-sdk'
29
+
30
+ const { data: created } = await api.dataActivationClients.create(
31
+ tenantSlug,
32
+ datalakeSlug,
33
+ {
34
+ name: 'Inbound CSV pipeline',
35
+ description: 'Manual CSV upload feeding the patient and appointment contracts',
36
+ tool_id: manualUploadToolId,
37
+ data_source_id: inboundDataSourceId,
38
+ tool_call: { tool_call_type: 'manual_upload' },
39
+ interoperability_contract_ids: [patientContractId, appointmentContractId],
40
+ cron_expressions: [], // omit for on-demand; e.g. ['0 7,19 * * *']
41
+ loop_over: [], // 'services' | 'locations' | 'providers'
42
+ row_filter: undefined, // Liquid pre-filter; see §2
43
+ downstream_connection_ids: [], // DAC chaining; cycle-checked
44
+ },
45
+ )
46
+ // created.id, created.slug — server-derived
47
+ // created.tool_id === body.tool_id
48
+ // created.interoperability_contracts — embedded list of the bound contracts
49
+ // created.is_default — true for platform-auto-created defaults
50
+ ```
51
+
52
+ ## 2. Rules the type cannot encode
53
+
54
+ ### `tool_call.tool_call_type` must align with the bound tool's `tool_body_type`
55
+
56
+ The activation client's `tool_call` discriminator selects which
57
+ call shape will be sent to the tool at run time. The valid
58
+ `tool_call_type` is determined by the bound tool's
59
+ `tool_body_type`:
60
+
61
+ ```
62
+ tool_body_type: 'manual_upload' → tool_call_type: 'manual_upload'
63
+ tool_body_type: 'sftp' → tool_call_type: 'sftp_fetch' | ...
64
+ tool_body_type: 'rest_api' → tool_call_type: 'rest_api_request'
65
+ tool_body_type: 'sns' → tool_call_type: 'sms_request'
66
+ ```
67
+
68
+ (See `tools.md` §5 for the polymorphic call shape per tool body
69
+ type.) A mismatch — e.g. a `manual_upload` tool bound with an
70
+ `sftp_fetch` call — is rejected at create time as a 422 on
71
+ `/tool_call/tool_call_type`.
72
+
73
+ ### `interoperability_contract_ids` is a virtual many-to-many array
74
+
75
+ The field is **caller-supplied** on `Request[Writable]` as a
76
+ `string[]` of contract UUIDs, but the **response** embeds the
77
+ bound contracts as an `interoperability_contracts` array of
78
+ full contract response objects. On `update()`, re-supply the
79
+ `interoperability_contract_ids` array verbatim — `update()`
80
+ replaces the whole array, so omitting it disassociates ALL
81
+ bound contracts.
82
+
83
+ Contracts and the activation client's tool / data source must
84
+ all live under the same `datalake_slug`; cross-datalake
85
+ references reject with a 404 on the offending id field.
86
+
87
+ ### `interoperability_contract_ids` composition is consumer-decided
88
+
89
+ A single activation client can bind:
90
+
91
+ - one or more `custom` / `system` contracts (e.g. patient +
92
+ appointment for a multi-resource ingestion), OR
93
+ - a single `identity` contract bound to a generic table
94
+ (the auto-created identity contract from
95
+ `interoperability_contracts.md` §5).
96
+
97
+ Mixing `identity` and non-identity contracts on one activation
98
+ client is semantically unusual — fan-out treats each contract
99
+ as a separate transformation lane, and identity contracts pass
100
+ rows through unchanged. The platform doesn't reject the mix,
101
+ but consumers typically split into two activation clients
102
+ (one per output table).
103
+
104
+ ### `row_filter` has INVERTED semantics
105
+
106
+ A `row_filter` is a Liquid template that decides whether a row
107
+ enters the activation pipeline AFTER the contract-level
108
+ `filter_template` runs. Like `filter_template`, the semantics
109
+ are inverted from most filter DSLs:
110
+
111
+ - **Empty render → PASS** (row enters the pipeline).
112
+ - **Non-empty render → SKIP** (row is dropped; the rendered
113
+ string is captured as the skip reason).
114
+
115
+ Liquid context includes `msg.row` (the inbound row) and
116
+ `msg.client` (the activation client's template vars). If you
117
+ omit `row_filter`, every row passes through.
118
+
119
+ Anchor on the empty-render-passes rule, not on the Liquid
120
+ keyword — `{% if ... %}...{% endif %}` and
121
+ `{% unless ... %}...{% endunless %}` are equivalent.
122
+
123
+ ## 3. Field ownership
124
+
125
+ **Server-derived (Response-only).** Universal set from
126
+ `type_naming.md`, plus:
127
+
128
+ ```
129
+ interoperability_contracts array of bound contract response
130
+ objects (mirror of the virtual
131
+ *_ids array on the request)
132
+ is_default boolean — true for platform-auto-
133
+ created default DACs (one per resource
134
+ type at datalake migration); cannot
135
+ be deleted, can be PUT-updated
136
+ ```
137
+
138
+ **Caller-supplied (round-trip).**
139
+
140
+ ```
141
+ name required string
142
+ description optional string
143
+ tool_id required UUID — bound tool
144
+ data_source_id required UUID — bound data source
145
+ tool_call required embed — { tool_call_type, ... }
146
+ interoperability_contract_ids required string[] — virtual many-to-many
147
+ cron_expressions optional string[] — Crontab syntax (e.g.
148
+ ['0 */6 * * *']); empty array = on-demand
149
+ loop_over optional enum[] — 'services' | 'locations' |
150
+ 'providers'; fan-out dimensions per run
151
+ row_filter optional string — Liquid pre-filter; see §2
152
+ for inverted semantics
153
+ downstream_connection_ids optional UUID[] — DACs triggered after this
154
+ one completes; cycles rejected at create
155
+ ```
156
+
157
+ **Write-only (Request-only).** None — every caller-supplied
158
+ field round-trips on the response (the virtual array
159
+ round-trips under `interoperability_contracts` on response).
160
+
161
+ ## 4. Error envelopes
162
+
163
+ Standard JSON:API envelopes per `errors.md`. Common rejections:
164
+
165
+ | `source.pointer` | Cause |
166
+ |-------------------------------------------|----------------------------------------------------|
167
+ | `/tool_id` / `/data_source_id` | resource doesn't exist (or wrong datalake) |
168
+ | `/tool_call/tool_call_type` | mismatch with the tool's `tool_body_type` |
169
+ | `/interoperability_contract_ids/0` | contract id doesn't exist or wrong datalake |
170
+ | `/name` | uniqueness within datalake (POST and PUT — rename can collide) |
171
+ | `/cron_expressions/0` | invalid crontab syntax |
172
+ | `/cron_expressions` | every-minute cron is rejected — **409 Conflict**, not 422; minimum interval enforced platform-side |
173
+
174
+ ## 5. Lifecycle
175
+
176
+ ### Create
177
+
178
+ Synchronous. The activation client is immediately runnable —
179
+ there's no async "deploy" phase. The next step is invoking one
180
+ of the runtime verbs in §6.
181
+
182
+ ### Read shapes
183
+
184
+ ```
185
+ .list(tenantSlug, datalakeSlug) Paged: { data, meta }
186
+ .get(tenantSlug, datalakeSlug, slug) One row, by slug
187
+ .metadata(tenantSlug, datalakeSlug, slug) Agent-facing metadata
188
+ ```
189
+
190
+ ### Update
191
+
192
+ `update()` replaces the whole resource — resupply the full
193
+ body; there's no partial update. Pointer fields
194
+ (`tool_id`, `data_source_id`, `interoperability_contract_ids`)
195
+ can be re-pointed.
196
+
197
+ **The `slug` regenerates from `name` on every `update()`.** The
198
+ lookup key you pass is the OLD slug (so the update call resolves),
199
+ but the response body carries the NEW slug derived from the
200
+ updated `name`. Examples:
201
+
202
+ ```
203
+ pre-update: name: 'Original', slug: 'original'
204
+ PUT body: name: 'Renamed', ...
205
+ PUT URL: .../data-activation-clients/original ← old slug works for lookup
206
+ PUT response: name: 'Renamed', slug: 'renamed' ← new slug derived
207
+ ```
208
+
209
+ **Consequence**: any downstream resource that references the
210
+ DAC by slug (workflow templates, action bodies referencing
211
+ `{{ ... }}` paths derived from the DAC slug, monitoring
212
+ dashboards, etc.) breaks the moment the DAC is renamed.
213
+ Capture the post-update slug from the response, NOT the lookup
214
+ key you passed. If you rename a DAC mid-deployment,
215
+ update every binding that references it.
216
+
217
+ An `update()` that keeps `name` unchanged regenerates to the
218
+ same slug (slugification is deterministic), so cosmetic-only
219
+ updates don't break bindings.
220
+
221
+ ### Delete
222
+
223
+ `delete()` removes the activation client. In-flight runs
224
+ decouple their lifecycle from the activation client row;
225
+ deleting one mid-run is generally rejected with a 422.
226
+
227
+ ## 6. Runtime
228
+
229
+ Three run shapes exist, selected by the bound tool's
230
+ `tool_body_type`:
231
+
232
+ ```
233
+ inline JSON → .ingest(slug, { data }) (small payloads)
234
+ uploaded file → .ingestFile(slug, { key }) (bulk; async)
235
+ pull-based → .runManually(slug) (e.g. SFTP, REST poll)
236
+ ```
237
+
238
+ All three produce **log rows** observable through
239
+ `api.dataActivationClients.logs.*` (§6.4), and the canonical way
240
+ to **verify** that ingestion worked is the two-step
241
+ dataset-search pattern in §6.5.
242
+
243
+ ### 6.1 Inline JSON ingestion — `.ingest`
244
+
245
+ For activation clients bound to a `manual_upload` tool, the
246
+ smallest run shape is a single JSON row submitted inline:
247
+
248
+ ```typescript
249
+ const { data } = await api.dataActivationClients.ingest(
250
+ tenantSlug, datalakeSlug, activationClientSlug,
251
+ { data: rowAsJson }, // a single row, shaped per the CSV/JSON the contracts expect
252
+ )
253
+ // data.batch_id — UUID; the row's batch identifier (use to find log rows)
254
+ // data.key — storage key for the row's archived ndjson
255
+ // data.jobs_count — number of downstream jobs the platform enqueued
256
+ ```
257
+
258
+ The response returns **immediately after enqueueing**, not
259
+ after ingestion completes. To know when the row has actually
260
+ been written, poll the logs (§6.4) or use the verification
261
+ pattern (§6.5).
262
+
263
+ The `batch_id` is the most important field on the response —
264
+ every downstream log row and dataset row carries the same
265
+ `batch_id`, so it's the join key for run-specific verification.
266
+
267
+ ### 6.2 Bulk file ingestion — `.ingestFile`
268
+
269
+ For larger payloads, upload the file via a presigned URL first,
270
+ then trigger ingestion by the returned key:
271
+
272
+ ```typescript
273
+ // 1. Mint a presigned upload URL (see datalakes.md).
274
+ const { data: link } = await api.datalakes.createUploadLink(
275
+ tenantSlug, datalakeSlug,
276
+ { content_type: 'text/csv', filename: 'inbound.csv' },
277
+ )
278
+ // link.url — presigned PUT URL (signature + expiry baked in)
279
+ // link.key — storage key the platform will reference
280
+
281
+ // 2. PUT the file body to the presigned URL (raw HTTP, NOT via the SDK).
282
+ await fetch(link.url, {
283
+ method: 'PUT',
284
+ headers: { 'Content-Type': 'text/csv' },
285
+ body: csvBytes,
286
+ })
287
+
288
+ // 3. Enqueue the ingest-file job by key.
289
+ const { data: job } = await api.dataActivationClients.ingestFile(
290
+ tenantSlug, datalakeSlug, activationClientSlug,
291
+ { key: link.key },
292
+ )
293
+ // job.key — echoes link.key
294
+ // job.job_id — numeric id of the enqueued background job
295
+ ```
296
+
297
+ This path is **fully async** — the response acknowledges that
298
+ the work has been enqueued, but no row has been written yet.
299
+ In the background the platform reads the file, allocates a fresh
300
+ `batch_id`, and fans out per-row work. Discover the assigned
301
+ `batch_id` by diffing the activation client's log rows before vs
302
+ after the call (§6.4).
303
+
304
+ ### 6.3 Manual run — `.runManually`
305
+
306
+ For activation clients bound to pull-based tools (`sftp`,
307
+ `rest_api`, etc.), there is no caller-supplied payload — the
308
+ tool itself fetches data from the external system. The SDK
309
+ verb:
310
+
311
+ ```typescript
312
+ const { data } = await api.dataActivationClients.runManually(
313
+ tenantSlug, datalakeSlug, activationClientSlug,
314
+ )
315
+ ```
316
+
317
+ Returns an acknowledgement; the actual fetch runs
318
+ asynchronously in the background. As with `.ingestFile`,
319
+ discover the assigned `batch_id` by polling the logs.
320
+
321
+ #### One-shot `tool_call` override
322
+
323
+ `.runManually` accepts an optional `tool_call` body that
324
+ overrides the DAC's persisted `tool_call` FOR THIS RUN ONLY.
325
+ Omitting the body (or omitting just the `tool_call` field) runs
326
+ with the persisted `tool_call`. Supplying a `tool_call` map
327
+ does NOT mutate the DAC row — it's a transient per-invocation
328
+ override, useful for one-off backfills with a different path,
329
+ payload, or fetch parameters.
330
+
331
+ ```typescript
332
+ await api.dataActivationClients.runManually(
333
+ tenantSlug, datalakeSlug, activationClientSlug,
334
+ { tool_call: { tool_call_type: 'rest_api_request', path: '/v1/backfill', ... } },
335
+ )
336
+ ```
337
+
338
+ ### 6.4 Logs subresource — `.logs.list` / `.logs.get`
339
+
340
+ Every ingestion run produces one or more **log rows** — one per
341
+ top-level interoperability contract that fired, plus one per
342
+ cascade target (a nested resource produced by a template's
343
+ upserts). The log row carries:
344
+
345
+ ```
346
+ batch_id UUID — the run's batch identifier
347
+ dataset_table string — the target dataset/table name
348
+ rows_ingested number — count of top-level rows the contract emitted
349
+ (cascade rows carry 0)
350
+ dataset_updated number — count of dataset rows whose batch_id this run wrote
351
+ (load-bearing: see Gotcha 7)
352
+ output_files string[] — merged ndjson archive paths
353
+ (empty until the post-batch merge worker commits)
354
+ ```
355
+
356
+ ```typescript
357
+ // Paginated list of log rows for an activation client.
358
+ const { data } = await api.dataActivationClients.logs.list(
359
+ tenantSlug, datalakeSlug, activationClientSlug,
360
+ )
361
+ // data.data — array of log rows; filter by batch_id to find this run
362
+
363
+ // Fetch one log row by id.
364
+ const { data: entry } = await api.dataActivationClients.logs.get(
365
+ tenantSlug, datalakeSlug, activationClientSlug, logId,
366
+ )
367
+ ```
368
+
369
+ #### Top-level rows vs cascade rows
370
+
371
+ A multi-resource contract (e.g. an inbound row that produces a
372
+ patient AND an appointment AND a related person) fires one
373
+ log row **per dataset_table** the contract touched. The
374
+ top-level contracts carry `rows_ingested > 0`; cascade rows
375
+ (nested upserts inside a template) carry `rows_ingested = 0`.
376
+ Filter on `rows_ingested > 0` when counting "how many top-level
377
+ contracts fired".
378
+
379
+ #### `output_files` as the completion signal
380
+
381
+ A log row appears as soon as the per-row write is acknowledged,
382
+ but `output_files` stays empty until the platform's background
383
+ merge step has committed the ndjson archive. Use
384
+ `output_files.length > 0` as the "this batch's rows are fully
385
+ durable" signal — not just the presence of the log row.
386
+
387
+ ### 6.5 Verifying ingestion: the two-step dataset search
388
+
389
+ The canonical way to **prove a row landed in the datalake** is
390
+ to query the dataset directly. This is a two-step pattern:
391
+
392
+ The dataset endpoints are datalake-slug-scoped: every call
393
+ takes `(tenantSlug, datalakeSlug, dataset, …)` as leading args.
394
+ There is no `datalake_id` query param and no tenant-derived
395
+ default — the datalake is mandatory and explicit, mirroring
396
+ `tools`, `workflows`, `dataActivationClients`, etc.
397
+
398
+ ```typescript
399
+ // (1) POST creates a user search and runs the underlying SQL
400
+ // synchronously against the regulated schema. The result
401
+ // set is materialised server-side and identified by the
402
+ // returned UUID.
403
+ const { data: search } = await api.datasets.createUserSearch(
404
+ tenantSlug,
405
+ datalakeSlug,
406
+ 'patient', // dataset name
407
+ {
408
+ search_query: `ri.value = '${externalPatientId}'` // SQL WHERE-clause fragment
409
+ + ` AND rp.batch_id = '${batchId}'`,
410
+ },
411
+ )
412
+ // search.id — UUID of the materialised result set
413
+ // search.status — 'completed' on success; 'failed' surfaces error_message
414
+ // search.results_count — ALWAYS null on the POST response (see Gotcha 5)
415
+
416
+ // (2) GET paginates rows from the materialised result set.
417
+ // dataAccessMode selects the storage tier:
418
+ // 'regulated' — raw values verbatim (PHI/PII intact)
419
+ // 'unregulated' — tokenized columns return as
420
+ // { redacted_data, token, type } objects;
421
+ // plain columns return as their primitive type
422
+ const { data: page } = await api.datasets.search(
423
+ tenantSlug,
424
+ datalakeSlug,
425
+ 'patient',
426
+ { userSearchId: search.id, dataAccessMode: 'unregulated' },
427
+ )
428
+ // page.data — array of rows
429
+ // page.meta — pagination
430
+ ```
431
+
432
+ #### SQL aliases
433
+
434
+ `createUserSearch` accepts a WHERE-clause fragment against a
435
+ fixed set of table aliases. Each dataset has a canonical base
436
+ alias for its regulated table, plus `ri` for the
437
+ regulated_identifiers join:
438
+
439
+ ```
440
+ patient → rp (regulated_patients) + ri
441
+ appointment → ra (regulated_appointments) + ri
442
+ customer → rc (regulated_customers) + ri
443
+ ... (one alias per regulated dataset)
444
+ ```
445
+
446
+ The `ri.value` predicate filters on external identifiers (the
447
+ values your inbound rows carried); the `<base_alias>.batch_id`
448
+ predicate scopes the search to one run.
449
+
450
+ #### Tokenized vs plain column shape
451
+
452
+ In `dataAccessMode: 'unregulated'`, tokenized columns surface
453
+ as objects, NOT primitives:
454
+
455
+ ```typescript
456
+ {
457
+ redacted_data: '*****', // server-redacted preview
458
+ token: 'tk_...', // stable opaque token
459
+ type: 'string' // original column's logical type — one of:
460
+ | 'text' // 'string' / 'text': free-form text content
461
+ | 'email' // 'email': RFC 5322 email
462
+ | 'phone' // 'phone': E.164 / display phone number
463
+ | 'date' // 'date': ISO 8601 date
464
+ | 'image', // 'image': image file reference
465
+ }
466
+ ```
467
+
468
+ Plain columns (non-tokenized) surface as the same primitive
469
+ the regulated schema stores. A consumer code path that
470
+ unconditionally treats every field as a string will crash on
471
+ tokenized columns — type-check `typeof row.field === 'object'`
472
+ first.
473
+
474
+ ## 7. Gotchas
475
+
476
+ 1. **`tool_call_type` must match the tool's `tool_body_type`.**
477
+ An activation client is a contract between the tool and the
478
+ pipeline; sending a call shape the tool doesn't accept is
479
+ rejected at create. See `tools.md` §5 for the per-body
480
+ call-shape table.
481
+
482
+ 2. **`interoperability_contract_ids` on `update()` replaces the
483
+ bound set entirely.** `update()` replaces the whole array —
484
+ resupply the full set; any item you omit is removed. Always
485
+ re-supply the full id array on update, even if you're only
486
+ changing `description`.
487
+
488
+ 3. **All referenced resources live under one datalake.** The
489
+ activation client, its tool, its data source, and every
490
+ bound contract must share the same parent datalake.
491
+ Cross-datalake references return 404 on the offending id.
492
+
493
+ 4. **Custom-dataset identity contracts get their own
494
+ activation client.** When you ingest into a generic table,
495
+ bind the table's auto-created identity contract (see
496
+ `interoperability_contracts.md` §5) to a dedicated activation
497
+ client. Don't mix it with non-identity contracts on the
498
+ same client unless you specifically want the fan-out
499
+ semantics.
500
+
501
+ 5. **`createUserSearch.results_count` is always null.** The
502
+ POST response confirms the search materialised but does
503
+ NOT report row counts. The actual rows live in the
504
+ server-side result set and only become visible via the
505
+ paginated `.search` GET. A common mistake is reading
506
+ `results_count` and concluding "no rows matched" when the
507
+ POST simply doesn't populate that field.
508
+
509
+ 6. **`.ingest` returns BEFORE the row is durable.** The
510
+ response carries `batch_id` + `jobs_count`, signalling that
511
+ downstream jobs have been enqueued — not that they've
512
+ completed. Poll logs (or use the search verification
513
+ pattern in §6.5) before treating the row as ingested.
514
+
515
+ 7. **`dataset_updated > 0` is the load-bearing freshness
516
+ gate.** `rows_ingested` flips as soon as the per-row job
517
+ completes, but the dataset row's `batch_id` column is
518
+ updated by a downstream trigger that can lag. Filtering a
519
+ `.createUserSearch` by `<base_alias>.batch_id = '<this>'`
520
+ BEFORE that trigger fires returns an empty result set,
521
+ and there's no way to refresh the materialised result —
522
+ you'd have to POST a new search. Poll for
523
+ `dataset_updated > 0` before issuing the search.
524
+
525
+ 8. **`output_files` empty ≠ batch failed.** A log row with
526
+ `rows_ingested > 0` but `output_files: []` is mid-merge,
527
+ not broken. The platform commits the ndjson archives
528
+ asynchronously; wait for `output_files.length > 0` if your
529
+ downstream code reads the archives directly.
530
+
531
+ 9. **Bulk uploads PUT to a presigned URL, NOT through the
532
+ SDK.** Step 2 of the bulk flow uses raw `fetch()` against
533
+ the presigned URL the platform returns; the SDK is NOT
534
+ involved in the upload itself. The SDK round-trip only
535
+ covers minting the URL (`createUploadLink`) and
536
+ triggering ingestion by key (`ingestFile`).
537
+
538
+ 10. **`runManually` and `.ingest` produce structurally
539
+ identical log rows.** Once a `batch_id` is allocated,
540
+ every run flavor merges into the same log/dataset row
541
+ shape. Consumers that diff or aggregate runs don't need to
542
+ branch on which verb produced the batch.
543
+
544
+ 11. **`row_filter` is empty-passes, non-empty-skips.** Same
545
+ inverted semantics as InteroperabilityContract.filter_template.
546
+ A correctly-shaped filter renders to a non-empty string when
547
+ the row should be SKIPPED. Always anchor on the
548
+ empty-render-passes rule, not on the Liquid keyword choice.
549
+
550
+ 12. **`downstream_connection_ids` is cycle-checked at create.**
551
+ The platform rejects DAC chains that would form a cycle. Make
552
+ sure both endpoints of the chain exist before adding the
553
+ pointer; cross-datalake chaining is also rejected.
554
+
555
+ 13. **`is_default: true` DACs cannot be deleted.** Platform-
556
+ auto-created default DACs (one per resource type at datalake
557
+ migration time) are protected. Filter them out
558
+ (`!d.is_default`) when listing for management purposes; you
559
+ can still PUT-update them (e.g. to bind additional contracts).