@alvera-ai/platform-sdk 0.16.3 → 0.17.0-next.g00fdd4c

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 (82) hide show
  1. package/.agent/AGENTS.md +88 -143
  2. package/.agent/account_management.md +2 -2
  3. package/.agent/action_logs.md +4 -4
  4. package/.agent/ai_agents.md +28 -21
  5. package/.agent/ai_sandbox.md +49 -39
  6. package/.agent/connected_apps.md +3 -3
  7. package/.agent/cookbook/_fixtures/README.md +1 -1
  8. package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_generic_table.liquid +1 -1
  9. package/.agent/cookbook/_fixtures/organic-marketing/_lead_submissions_foundation_legal_entity.liquid +80 -0
  10. package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_mdm.liquid +2 -1
  11. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_generic_table.liquid +57 -0
  12. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_legal_entity.liquid +30 -0
  13. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_mdm.liquid +44 -0
  14. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_generic_table.liquid +57 -0
  15. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_legal_entity.liquid +36 -0
  16. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_mdm.liquid +41 -0
  17. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_generic_table.liquid +70 -0
  18. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_legal_entity.liquid +52 -0
  19. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_mdm.liquid +42 -0
  20. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_generic_table.liquid +38 -0
  21. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_legal_entity.liquid +48 -0
  22. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_mdm.liquid +49 -0
  23. package/.agent/cookbook/organic-marketing.md +2801 -0
  24. package/.agent/cookbook/payments-compliance.md +2180 -0
  25. package/.agent/cookbook/primary-care.md +2175 -0
  26. package/.agent/cookbook/subscription-saas.md +2403 -0
  27. package/.agent/data_activation_clients.md +65 -52
  28. package/.agent/datalakes.md +338 -171
  29. package/.agent/errors.md +3 -3
  30. package/.agent/generic_tables.md +151 -62
  31. package/.agent/interoperability_contracts.md +57 -22
  32. package/.agent/mdm.md +136 -153
  33. package/.agent/messages.md +81 -40
  34. package/.agent/mock-services.md +192 -0
  35. package/.agent/mutations.md +2 -2
  36. package/.agent/templates.md +14 -13
  37. package/.agent/tools.md +63 -21
  38. package/.agent/type_naming.md +13 -13
  39. package/.agent/workflows.md +99 -53
  40. package/dist/bin/platform-sdk.mjs +33 -47
  41. package/dist/bin/platform-sdk.mjs.map +1 -1
  42. package/dist/index.d.mts +521 -377
  43. package/dist/index.d.mts.map +1 -1
  44. package/dist/index.mjs +493 -59
  45. package/dist/index.mjs.map +1 -1
  46. package/package.json +4 -3
  47. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +0 -88
  48. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +0 -47
  49. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +0 -24
  50. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +0 -38
  51. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_compliance_screening.liquid +0 -59
  52. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_mdm.liquid +0 -36
  53. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_mdm.liquid +0 -30
  54. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_payment_account.liquid +0 -55
  55. package/.agent/cookbook/_fixtures/subscription/_customers_subscription_mdm.liquid +0 -20
  56. package/.agent/cookbook/_setup/foundation.md +0 -359
  57. package/.agent/cookbook/_setup/healthcare.md +0 -361
  58. package/.agent/cookbook/_setup/payments.md +0 -365
  59. package/.agent/cookbook/_setup/subscription.md +0 -364
  60. package/.agent/cookbook/action-status-updaters.md +0 -278
  61. package/.agent/cookbook/ai-agent-invoke.md +0 -279
  62. package/.agent/cookbook/appointment-review-sms-workflow.md +0 -801
  63. package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -696
  64. package/.agent/cookbook/bulk-ingest.md +0 -302
  65. package/.agent/cookbook/contact-us-triage-with-llm.md +0 -663
  66. package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -659
  67. package/.agent/cookbook/generic-tables.md +0 -244
  68. package/.agent/cookbook/invite-team.md +0 -200
  69. package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -661
  70. package/.agent/cookbook/marketing-campaign-send.md +0 -1044
  71. package/.agent/cookbook/paginated-restapi-poller.md +0 -383
  72. package/.agent/cookbook/rest-fetch.md +0 -273
  73. package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -773
  74. package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -665
  75. package/.agent/cookbook/system-templates.md +0 -165
  76. package/.agent/cookbook/talk-to-data.md +0 -178
  77. package/.agent/cookbook/triage-prospects-by-priority.md +0 -571
  78. package/.agent/cookbook/welcome-sms-for-customers.md +0 -647
  79. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/memorandum-of-association-01.png +0 -0
  80. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/sample_two_page.pdf +0 -0
  81. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/_customers_subscription_customer.liquid +0 -0
  82. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/stripe_customers_batch1.csv +0 -0
package/.agent/AGENTS.md CHANGED
@@ -231,17 +231,16 @@ What changes between them is only *where you put the calls* (a component
231
231
  handler vs. a test body), never the calls themselves. The cookbooks under
232
232
  `cookbook/` read as test-style sequences because that is how they are
233
233
  validated, but every step is a plain `api.*` call you can lift into an app
234
- unchanged. The per-capability docs (e.g. `bulk-ingest.md`, `ai-agent-invoke.md`)
235
- show one capability at a time; the cookbooks weave capabilities into a
236
- business outcome.
234
+ unchanged. Capabilities are not documented separately — each lives as
235
+ numbered steps inside the cookbook whose use case motivates it.
237
236
 
238
237
  ## Resources
239
238
 
240
239
  ```
241
240
  INFRASTRUCTURE (stood up before data flows)
242
241
  ──────────────
243
- datalakes.md Storage layer; regulated +
244
- unregulated tiers.
242
+ datalakes.md Storage layer; the raw lake and
243
+ its tokenized + redacted copies.
245
244
  data_sources.md External ingestion endpoints.
246
245
  tools.md Authenticated connections for
247
246
  action execution.
@@ -323,12 +322,13 @@ at runtime.
323
322
 
324
323
  Each cookbook structure:
325
324
 
326
- - **Front matter** — `title`, `summary`, `industry`, `slug`,
325
+ - **Front matter** — `title`, `summary`, `use_case`, `slug`,
327
326
  `vitest_source` (list of integration-test files the snippets
328
- are lifted from, anchor file first), `status`. The `industry:`
329
- field also drives automatic discovery of the per-industry
330
- bootstrap setup file at `_setup/<industry>.md` — see
331
- "Industry-derived setup files" below.
327
+ are lifted from, anchor file first), `status`. `use_case` is one
328
+ of the four surfaces `organic-marketing`,
329
+ `subscription-saas`, `payments-compliance`,
330
+ `primary-care-feedback` and there is exactly one cookbook per
331
+ use case, at `cookbook/<use-case>.md`.
332
332
  - **Problem** — the business-outcome statement in domain terms,
333
333
  sourced from the anchor vitest's behaviour (not from
334
334
  customer-narrative documentation).
@@ -349,44 +349,33 @@ Each cookbook structure:
349
349
  - **See also** — links to relevant per-resource reference MDs
350
350
  and to the anchor + ancillary vitest files.
351
351
 
352
- ### Industry-derived setup files
353
-
354
- Cookbook authoring uses per-industry bootstrap setup files
355
- under `.agent/cookbook/_setup/<industry>.md` to DRY out the
356
- auth + tenant + datalake + dataset-seeding steps every
357
- scenario in an industry shares. The convention matches the
358
- markdown-doctest ecosystem pattern — setup belongs to a
359
- scope (the industry), and any cookbook in that scope
360
- inherits the setup by being there. No per-cookbook opt-in
361
- field is required.
362
-
363
- - Setup files live under `_setup/`. The leading underscore on
364
- the directory marks them as fragments (not standalone
365
- scenarios); the validator's discovery walk skips them and
366
- the corpus index does not list them under "Available
367
- cookbooks."
368
- - The validator reads each scenario cookbook's existing
369
- `industry:` front-matter field and auto-discovers the
370
- setup file at `_setup/<industry>.md`. A cookbook in an
371
- industry that has no setup file gets nothing inlined; the
372
- validator does not fail if the setup file is absent.
373
- Cookbook authors write no additional front-matter for
374
- setup inclusion the relationship is implicit, by
375
- convention.
376
- - The validator inlines the setup file's numbered `it()`
377
- blocks BEFORE the scenario's own numbered `it()` blocks
378
- inside the same `describe(...)`. Each `it()` label is
379
- prefixed with its source slug so failure output
380
- unambiguously points at the file to open (e.g.
381
- `_setup/foundation §001 — auth` versus
382
- `birthday-greeting-sms-trigger §001 — create workflow`).
383
- Cookbook authors keep clean local §001-§00N numbering
384
- inside their own markdown.
385
- - An agent reading a scenario cookbook discovers the matching
386
- setup file at the predictable conventional path
387
- `_setup/<industry>.md` — one extra file open at a
388
- fixed-by-convention location, not via cookbook-specific
389
- metadata the agent has to learn.
352
+ ### Every cookbook is self-reliant, and repeats what it needs
353
+
354
+ A cookbook stands up **its own** tenant, datalake and resources in
355
+ its own numbered steps. There is no shared setup file, no setup
356
+ group and no bootstrap phase: everything the walk depends on is
357
+ in the walk.
358
+
359
+ That means the auth + tenant + datalake preamble is **duplicated
360
+ across all four cookbooks, deliberately.** This corpus is read by
361
+ someone or something — that opens one file and works through it,
362
+ and for that reader a shared setup is not a saving. It is a second
363
+ file to find, a second convention to learn, and a hidden dependency
364
+ on steps they never see. Four documents repeating a preamble is the
365
+ cheaper trade, and it is the trade this corpus makes. Do not
366
+ re-factor it out.
367
+
368
+ Two consequences worth stating plainly:
369
+
370
+ - **Write for idempotence, not just for a clean run.** Use stable
371
+ names and create-or-reuse: look the resource up, create it only
372
+ if it is absent, and assert the end state either way. A step that
373
+ mints a fresh random name works once and litters forever —
374
+ nothing in the product reclaims an abandoned datalake. Ask of
375
+ every creating step: what does this do on run two?
376
+ - **Label numbering is local and clean.** Each cookbook numbers its
377
+ own steps `§001…§00N` from the top, and a failure names the file
378
+ and the step directly.
390
379
 
391
380
  ### Vendored fixtures (Liquid templates, CSV bodies)
392
381
 
@@ -417,7 +406,7 @@ repo's own integration tests use.
417
406
  const LE_TEMPLATE = fs.readFileSync(
418
407
  path.join(
419
408
  process.env.COOKBOOK_FIXTURES_DIR!,
420
- 'foundation/_lead_submissions_foundation_legal_entity.liquid',
409
+ 'organic-marketing/_lead_submissions_foundation_legal_entity.liquid',
421
410
  ),
422
411
  'utf8',
423
412
  )
@@ -439,109 +428,58 @@ repo's own integration tests use.
439
428
  template plus the cookbooks that depend on it. No need to
440
429
  read cookbook code first to know what fixtures exist.
441
430
 
442
- ### Two kinds of doc here: business cookbooks vs. capability docs
431
+ ### The four cookbooks
443
432
 
444
- The `cookbook/` directory holds two kinds of recipe, both validated by
445
- `make validate-cookbook`:
433
+ `cookbook/` holds **four files, one per use case**, and nothing else that
434
+ runs. Each is self-reliant: it stands up its own tenant, datalake and
435
+ resources in its own numbered steps and shares nothing with its siblings.
436
+ A reader reads one file end to end and needs no second one. The
437
+ duplicated bootstrap across them is deliberate — four repeated preambles
438
+ are cheaper than one shared setup every reader must go and understand
439
+ first.
446
440
 
447
- - **Business cookbooks** (`<use-case>.md`) one real business outcome each,
448
- told as a numbered API-call sequence (e.g. `welcome-sms-for-customers`,
449
- `dunning-sms-for-delinquent`). Read these to see how resources compose into
450
- an outcome.
451
- - **Capability docs** (`<capability>.md`) one platform capability each,
452
- shown as the minimal call sequence that proves it (e.g. `bulk-ingest`,
453
- `ai-agent-invoke`, `generic-tables`, `invite-team`). Read these to learn one
454
- capability in isolation; cookbooks weave them into outcomes.
441
+ There is no separate per-capability file. Bulk upload, REST fetch, team
442
+ invitations, action status updaters, paginated pollers, text-to-SQL and
443
+ agent file-invocation each live as numbered steps inside the cookbook
444
+ whose use case motivates them, because a capability shown without a
445
+ reason to use it is the thing readers skip.
455
446
 
456
- ### Available cookbooks
457
-
458
- The ten business-cookbook scenarios. Each is anchored to a green
459
- end-to-end vitest scenario in the platform's integration-tests suite and is
460
- verified at dev time by `make validate-cookbook` at the platform-sdk repo root.
461
- This index is mirrored into the managed block `alvera llm-export` writes to
462
- consumer `AGENTS.md` files (`buildManagedBlock` in both the SDK and CLI
463
- packages — kept in sync by hand; the llm-export test suites pin the shipped
464
- copy, so a dropped entry breaks a test).
447
+ All four are verified at dev time by `make validate-cookbook` at the
448
+ repo root; every numbered step is one test.
465
449
 
466
450
  <!-- BEGIN:cookbook-index -->
467
451
 
468
- **Healthcare**
469
-
470
- - [appointment-review-sms-workflow](./cookbook/appointment-review-sms-workflow.md)
471
- Send a patient review-request SMS after a fulfilled
472
- appointment, deep-linked to a connected-app feedback form.
473
- - [contact-us-triage-with-llm](./cookbook/contact-us-triage-with-llm.md)
474
- Triage inbound contact-us messages into three priority
475
- buckets (appointment / job-application / spam) via an LLM
476
- agent, route each to a tailored SMS action.
477
-
478
- **Subscription**
479
-
480
- - [welcome-sms-for-customers](./cookbook/welcome-sms-for-customers.md)
481
- Send a welcome SMS to newly contracted customers with a
482
- self-serve billing-portal link.
483
- - [dunning-sms-for-delinquent](./cookbook/dunning-sms-for-delinquent.md)
484
- Send a payment-reminder SMS to delinquent customers
485
- (filtered on phone-on-file and tax-id-verified) with a
486
- pay-invoice link.
487
- - [triage-prospects-by-priority](./cookbook/triage-prospects-by-priority.md)
488
- Triage inbound AR customers into priority bands
489
- (high / medium / low) via an LLM agent, route each band to a
490
- tailored SMS action.
491
-
492
- **Payments**
493
-
494
- - [kyc-notification-on-account-activation](./cookbook/kyc-notification-on-account-activation.md)
495
- — Send a KYC-notification SMS when a payment account
496
- transitions to active status.
497
- - [sanctions-screening-with-agent-review](./cookbook/sanctions-screening-with-agent-review.md)
498
- — Disambiguate gray-zone sanctions screenings via an LLM
499
- agent, route confirmed-clean and confirmed-block outcomes to
500
- distinct SMS actions.
501
-
502
- **Foundation**
503
-
504
- - [birthday-greeting-sms-trigger](./cookbook/birthday-greeting-sms-trigger.md)
505
- — Send a happy-birthday SMS on each contact's next birthday
506
- using a pure-Liquid trigger (year-roll math).
507
- - [score-leads-with-llm-categorization](./cookbook/score-leads-with-llm-categorization.md)
508
- — Score inbound leads into four bands
509
- (hot / warm / cold / spam) via an LLM agent, route each band
510
- to a tailored SMS action.
511
- - [marketing-campaign-send](./cookbook/marketing-campaign-send.md)
512
- — Send an A/B marketing campaign across SMS and email: the
513
- suppression and reachability gates and the A/B split all live
514
- in the workflow, each send carries a per-recipient short link,
515
- and the reply re-attaches to the customer who sent it.
452
+ - [organic-marketing](./cookbook/organic-marketing.md) — score inbound
453
+ leads into four bands with an LLM agent, schedule a birthday greeting a
454
+ year out with a pure-Liquid trigger, and run an A/B campaign whose
455
+ suppression gates and split live in the workflow. Ends with delivery
456
+ reconciliation and a natural-language read of the lake. One raw
457
+ datalake.
458
+ - [payments-compliance](./cookbook/payments-compliance.md)
459
+ disambiguate gray-zone sanctions screenings with an LLM agent that
460
+ never sees who was screened, and fire a KYC notification on account
461
+ activation. Ends by reading one row through raw, tokenized and redacted
462
+ to show what each reader gets. Three datalakes.
463
+ - [subscription-saas](./cookbook/subscription-saas.md) — one customers
464
+ table and three workflows over it: a welcome SMS gated on
465
+ reachability, a dunning reminder gated on reachability AND KYC, and an
466
+ LLM agent banding accounts by priority. Then the same table filled
467
+ three more ways — inline, bulk CSV, REST fetch — and a teammate invited
468
+ onto the tenant. One raw datalake.
469
+ - [primary-care](./cookbook/primary-care.md) a review request that
470
+ only reaches patients whose visit actually happened, an LLM agent
471
+ triaging inbound messages off the tokenized projection, and a vision
472
+ agent reading a scanned document. Three datalakes.
516
473
 
517
474
  <!-- END:cookbook-index -->
518
475
 
519
- ### Capability docs
520
-
521
- One platform capability each the minimal call sequence that proves it,
522
- anchored to a green vitest scenario. Read the matching reference MD for the
523
- full wire shape.
524
-
525
- - [bulk-ingest](./cookbook/bulk-ingest.md) load a whole file of records in
526
- one upload (presigned link → PUT → `ingestFile` → search by batch).
527
- - [rest-fetch](./cookbook/rest-fetch.md) — pull records from a third-party REST
528
- API on demand (Bearer + OAuth2), no file upload.
529
- - [ai-agent-invoke](./cookbook/ai-agent-invoke.md) — read an uploaded
530
- document/image and pull structured JSON out of it (`aiAgents.invoke` with
531
- files). The correct file-vision path — the agent reads the file; the DAC
532
- ingests it.
533
- - [generic-tables](./cookbook/generic-tables.md) — stand up a generic table the
534
- built-in datasets don't model; deploy → ingest → read back via executeSql.
535
- - [action-status-updaters](./cookbook/action-status-updaters.md) — reconcile
536
- the delivery status of messages you send, on a schedule.
537
- - [system-templates](./cookbook/system-templates.md) — discover the platform's
538
- built-in row-mapping Liquid templates.
539
- - [invite-team](./cookbook/invite-team.md) — invite a teammate into your tenant
540
- (root / tenantless / tenant-scoped sessions in one flow).
541
- - [talk-to-data](./cookbook/talk-to-data.md) — turn a datalake conversational:
542
- natural language → reviewable SQL (`datalakes.textToSql`, data-free by
543
- construction) → read-only execution returning a `{ data, meta }` row page or a
544
- CSV export (`datalakes.executeSql`).
476
+ This index is mirrored into the managed block `alvera llm-export` writes
477
+ to consumer `AGENTS.md` files — `buildManagedBlock`, duplicated in both
478
+ the SDK and CLI packages and kept in sync by hand. **Both suites now
479
+ assert every cookbook named in the block exists on disk, and that every
480
+ cookbook on disk is named.** They previously pinned names against the
481
+ block string alone, which is how the index went on advertising four
482
+ deleted files without a single test failing.
545
483
 
546
484
  ## Utility namespaces
547
485
 
@@ -576,6 +514,13 @@ of restating:
576
514
  | Async + readiness | `async.md` |
577
515
  | Debugging (HTTP interceptor + safe redaction) | `debugging.md` |
578
516
  | Type naming + server-derived fields | `type_naming.md` |
517
+ | Local mock surfaces (WireMock / LocalStack) | `mock-services.md` |
518
+
519
+ `mock-services.md` answers the local-development questions the other
520
+ pages assume away: how the platform reaches mocked third-party APIs on
521
+ a laptop, the `endpoint_url` override on the sender tools, and why End
522
+ User Messaging points at WireMock rather than LocalStack. It is fetched
523
+ from the platform-metadata channel, not authored here.
579
524
 
580
525
  ## Just getting started?
581
526
 
@@ -86,7 +86,7 @@ platform-admin side door (a root Bearer):
86
86
  ```typescript
87
87
  const { data: minted } = await rootApi.admin.createTenantApiKey(tenantSlug, {
88
88
  name: 'Bootstrap Key',
89
- data_access_mode: 'unregulated',
89
+ data_access_mode: 'raw',
90
90
  })
91
91
  // minted.api_key — the publishable key value (thread into createSession)
92
92
  ```
@@ -329,7 +329,7 @@ What you SUBMIT when creating an invitation. The
329
329
  | Invitation `role` | Result on accept |
330
330
  |-------------------|----------------------------------------|
331
331
  | `'member'` | Standard tenant user |
332
- | `'researcher'` | Tokenized-only access; blocked from regulated schema |
332
+ | `'researcher'` | Ceiling of `tokenized`; cannot read the raw lake |
333
333
  | `'admin'` | Full tenant management |
334
334
 
335
335
  ```typescript
@@ -39,7 +39,7 @@ batch_id — the run this outcome belongs to; THE
39
39
  decision_key — which action fired
40
40
  context_key — companion string key; like decision_key, it is
41
41
  SQL-lane only (neither is Flop-filterable, §5)
42
- mdm_subject_id, subject_name, session_id
42
+ legal_entity_id, subject_name, session_id
43
43
  action_type ('sms'|'email'|'voice'|'data_exchange'), channel,
44
44
  classification
45
45
  status — see §3
@@ -92,17 +92,17 @@ message.
92
92
  // dataset type is SINGULAR 'action_log' (the table is plural)
93
93
  const { data: search } = await api.datasets.createUserSearch(
94
94
  tenantSlug, datalakeSlug, 'action_log',
95
- { search_query: `ral.batch_id = '${batchId}'` },
95
+ { search_query: `al.batch_id = '${batchId}'` },
96
96
  )
97
97
  const { data: page } = await api.datasets.search(
98
98
  tenantSlug, datalakeSlug, 'action_log',
99
- { userSearchId: search.id, dataAccessMode: 'unregulated' },
99
+ { userSearchId: search.id, dataAccessMode: 'tokenized' },
100
100
  )
101
101
  ```
102
102
 
103
103
  Filterable columns (Flop, the full set): `id`, `inserted_at`,
104
104
  `action_type`, `channel`, `status`, `batch_id`, `classification`,
105
- `workflow_id`, `sent_at`, `external_id`, `mdm_subject_id`. Note
105
+ `workflow_id`, `sent_at`, `external_id`, `legal_entity_id`. Note
106
106
  there is **no `global_search`** here (unlike the message dataset) —
107
107
  `decision_key`/`context_key` are not in this set either; scope them
108
108
  via the `search_query` SQL fragment above.
@@ -31,7 +31,7 @@ const { data: created } = await api.aiAgents.create(
31
31
  tool_id: chatCompletionToolId, // see §2 — must be a tool with intent: 'llm_enrichment'
32
32
 
33
33
  model: 'qwen3-vl:8b-instruct', // provider-specific model identifier
34
- data_access: 'unregulated', // 'unregulated' | 'regulated' (compliance gate)
34
+ data_access: 'tokenized', // 'raw' | 'tokenized' (compliance gate)
35
35
  temperature: 0.0,
36
36
  max_tokens: 2048,
37
37
  enabled: true,
@@ -125,25 +125,30 @@ referenced by the prompt body resolves to a property declared in
125
125
  ### `data_access` is the compliance gate
126
126
 
127
127
  ```
128
- data_access: 'unregulated' — the LLM call receives tokenized,
129
- redacted data; safe for any LLM
130
- (third-party, on-prem, …).
131
- Default for triage / categorization
132
- / lead-scoring workflows.
133
- data_access: 'regulated' — the LLM call receives raw values;
134
- MUST be paired with an LLM the
135
- operator trusts with this tier
136
- (typically an on-prem deployment).
137
- Use for compliance workflows that
138
- need raw values — sanctions
139
- screening, manual review, KYC.
128
+ data_access: 'tokenized' — the LLM call reads the tokenized lake:
129
+ each masked column arrives as a stable
130
+ tok_<hash>. Safe for any LLM, including
131
+ third-party. The right choice for triage,
132
+ categorization and lead scoring, where the
133
+ model reads free text and has no business
134
+ knowing whose it is.
135
+ data_access: 'raw' — the LLM call receives real values. MUST be
136
+ paired with an LLM the operator trusts
137
+ with them (typically on-prem). For work
138
+ that genuinely needs the value — sanctions
139
+ screening, manual review, KYC — and for
140
+ prompts that carry no identity at all, so
141
+ there is nothing to hold back.
142
+
143
+ An agent is never pointed at the redacted lake: its placeholders are
144
+ unjoinable by design, which makes them useless to a model.
140
145
  ```
141
146
 
142
147
  **The platform does NOT enforce `data_access` as immutable — a PUT
143
148
  `update()` can change it today.** The field is cast by the same
144
149
  changeset that `update()` uses and carries no immutability guard (and
145
- isn't `readOnly` on the wire), so an update can flip `regulated
146
- unregulated`, which changes whether the agent sees raw or tokenized
150
+ isn't `readOnly` on the wire), so an update can flip `raw
151
+ tokenized`, which changes whether the agent sees real or masked
147
152
  values. **Treat it as immutable by discipline** — a data source
148
153
  built expecting one tier shouldn't be silently re-tiered under it —
149
154
  but know the platform won't stop a flip, so guard it in your own
@@ -209,9 +214,10 @@ const { data: workflow } = await api.workflows.create(
209
214
  tenantSlug, datalakeSlug,
210
215
  {
211
216
  name: 'Contact-Us Triage',
212
- dataset_type: 'generic_table',
217
+ description: 'Triages inbound contact-us messages.',
218
+ dataset_type: 'generic_table' as const,
219
+ status: 'live' as const,
213
220
  tags: [],
214
- // …other workflow fields…
215
221
  workflow_ai_agents: [
216
222
  {
217
223
  ai_agent_id: createdAgentId,
@@ -305,7 +311,7 @@ name required string
305
311
  description optional string
306
312
  tool_id required UUID — must be intent: llm_enrichment
307
313
  model required string — provider-specific identifier
308
- data_access required enum — 'unregulated' | 'regulated'
314
+ data_access required enum — 'raw' | 'tokenized' (no default)
309
315
  temperature required number
310
316
  max_tokens required number
311
317
  enabled required bool
@@ -417,8 +423,9 @@ usage object — { model, input_tokens, output_tokens, total_tokens,
417
423
 
418
424
  Invoke passes NO `context_mapping_config` — `input` is validated
419
425
  directly against `input_schema` (unlike the nested workflow path,
420
- which remaps host context first). See the `ai-agent-invoke`
421
- cookbook for the full upload → tool → agent → invoke walk.
426
+ which remaps host context first). See `cookbook/primary-care.md`
427
+ §025–028 for the full upload → tool → agent → invoke walk, including
428
+ the input-gate refusal.
422
429
 
423
430
  ### Drift checksum
424
431
 
@@ -544,7 +551,7 @@ with `{ status: 'skipped' }` and no per-agent entries.
544
551
  6. **`data_access` is immutable by convention only — the platform
545
552
  won't stop a PUT from flipping it.** There's no immutability
546
553
  guard in the changeset, so an `update()` can change the
547
- compliance tier (`regulatedunregulated`) and re-tier what the
554
+ compliance tier (`rawtokenized`) and re-tier what the
548
555
  agent sees. Enforce it in your own change-control (or delete +
549
556
  recreate); don't rely on the platform to reject the flip (§2).
550
557
 
@@ -17,16 +17,16 @@ the worst case — and they compose into defense-in-depth.
17
17
 
18
18
  ## Layer 1 — Compliance gates (who sees what)
19
19
 
20
- Two consumer-set fields decide whether a request reaches raw
21
- PHI/PII/PCI or only the tokenized projection. Both must
22
- authorize regulated reach for raw data to flow:
20
+ Two consumer-set fields decide whether a request reaches the raw
21
+ lake or only the tokenized projection. Both must authorize raw reach
22
+ for real values to flow:
23
23
 
24
24
  ```
25
25
  session role AI agent data_access → outcome
26
26
  ────────────── ─────────────────── ───────
27
- member or admin regulated regulated reachable
28
- member or admin unregulated tokenized only
29
- member or admin (no agent on path) regulated reachable
27
+ member or admin raw raw reachable
28
+ member or admin tokenized tokenized only
29
+ member or admin (no agent on path) raw reachable
30
30
  researcher any value tokenized only
31
31
  ```
32
32
 
@@ -34,21 +34,28 @@ authorize regulated reach for raw data to flow:
34
34
  time (vocabulary A — `'member' | 'researcher' | 'admin'`; the
35
35
  resulting session carries the corresponding role-record name
36
36
  — see `account_management.md` §3).
37
- - **AI agent `data_access`** is `'regulated' | 'unregulated'`,
38
- pinned at agent creation (immutable post-create see
39
- `ai_agents.md`). Default is `'unregulated'`; `'regulated'` is
40
- opt-in.
41
-
42
- A `researcher` session can never reach raw PHI regardless of how
43
- the agent is configured. An `'unregulated'` agent can never
44
- reach raw PHI regardless of session role. Tenant isolation
45
- (every row carries a tenant id; queries filter on the session's
46
- tenant) AND-composes with both of these so a request only sees
47
- the slice that *all three* allow.
48
-
49
- The composition rule is fail-safe: a workflow spec that omits
50
- `data_access` is treated as `'unregulated'`. Author-side
51
- mistakes default to less reach, never more.
37
+ - **AI agent `data_access`** is `'raw' | 'tokenized'`, set at agent
38
+ creation. **The platform does not enforce it as immutable**one
39
+ `changeset/2` serves create and update, and nothing refuses a PUT
40
+ that flips the tier, so treat it as immutable by your own
41
+ change-control rather than by the platform's (see `ai_agents.md`
42
+ §2). An
43
+ agent is **never pointed at the redacted lake**: that copy is for a
44
+ person to glance at, and its values are unjoinable by design.
45
+
46
+ **There is no default the field is required.** Omitting it is a
47
+ 422 at create, not a quiet fall-back to the safer tier. That is a
48
+ deliberate change from an older design that defaulted to the
49
+ tokenized side: a default meant an author could get a compliance
50
+ tier without ever deciding on one, and the reviewer reading the
51
+ spec could not tell an intentional choice from an omission. Being
52
+ made to say it is the point.
53
+
54
+ A `researcher` session can never reach real values regardless of how
55
+ the agent is configured. A `'tokenized'` agent can never reach them
56
+ regardless of session role. Tenant isolation (every row carries a
57
+ tenant id; queries filter on the session's tenant) AND-composes with
58
+ both, so a request only sees the slice that *all three* allow.
52
59
 
53
60
  ## Layer 2 — Liquid sandbox (what can execute)
54
61
 
@@ -156,10 +163,9 @@ fragments. Either path lands at the same boundary.
156
163
 
157
164
  ### What SQL fragments CAN do
158
165
 
159
- - SELECT against the regulated tables exposed by the dataset
160
- search aliases (`rp` for `regulated_patients`, `rc` for
161
- `regulated_customers`, etc. — see `data_activation_clients.md`
162
- §6.5 SQL aliases).
166
+ - SELECT against the tables exposed by the dataset search aliases
167
+ (`le` for legal entities, `m` for messages, `al` for action logs
168
+ — see `data_activation_clients.md` §6.5 SQL aliases).
163
169
  - Filter results into the platform's `search_results`
164
170
  accumulator scoped to the materialised search id. This is the
165
171
  one write the query path performs, and the platform builds it
@@ -199,15 +205,15 @@ it sits inside the same Layer-3 boundary:
199
205
  only the natural-language `prompt` plus the datalake **schema**
200
206
  to the LLM — never rows. The model returns SQL for review; no
201
207
  datalake data ever crosses the LLM boundary, so generation is
202
- safe in both `regulated` and `unregulated` mode. The two-call
208
+ safe in every mode. The two-call
203
209
  split (generate, then execute) exists so a human or agent can
204
210
  inspect and edit the SQL before any data is read.
205
211
  - **Execution is read-only and mode-routed.** `executeSql` runs the
206
212
  statement against the mode-appropriate schema with a read-only
207
213
  connection: INSERT/UPDATE/DELETE/DDL are rejected (app-layer
208
214
  `@deny` + a read-only DB transaction + an EXPLAIN preflight), and
209
- `mode` (`regulated` vs `unregulated`) plus the session's tenant
210
- (RLS) decide which schema and which rows are reachable — the same
215
+ `mode` (`raw`, `tokenized` or `redacted`) plus the session's tenant
216
+ (RLS) decide which lake and which rows are reachable — the same
211
217
  Layer-1 compliance gates that govern every other read.
212
218
  - **Pagination caps blast radius.** `page` / `page_size` map to a
213
219
  server-side `LIMIT/OFFSET` window capped at the platform default,
@@ -220,9 +226,9 @@ it sits inside the same Layer-3 boundary:
220
226
  │ Layer 1 — compliance gates │
221
227
  │ session role AND agent data_access │
222
228
  │ AND tenant id (RLS) │
223
- │ Decides: which schema (regulated vs
224
- unregulated) and which tenant the query
225
- hits.
229
+ │ Decides: which lake (raw / tokenized /
230
+ redacted) and which tenant the query hits.
231
+
226
232
  └────────────────────┬────────────────────────┘
227
233
 
228
234
  ┌────────────────────▼────────────────────────┐
@@ -248,9 +254,9 @@ it sits inside the same Layer-3 boundary:
248
254
 
249
255
  Each layer is independently sufficient: bypassing any single
250
256
  layer still leaves the other two enforcing the contract. The
251
- compound rule is "all three must permit" — read PHI requires
252
- authorising session role AND regulated-mode agent (when an
253
- agent is on the path) AND tenant id match.
257
+ compound rule is "all three must permit" — reading real values
258
+ requires an authorising session role AND a `raw` agent (when an
259
+ agent is on the path) AND a tenant id match.
254
260
 
255
261
  ## Auditability
256
262
 
@@ -259,15 +265,19 @@ recorded with cloud-storage pointers to the exact input payload
259
265
  the agent received and the exact response the provider
260
266
  returned. The audit record outlives the host run that produced
261
267
  it — a compliance auditor can later replay any agent's input
262
- and verify what data class (regulated vs tokenized) actually
263
- reached the model. The audit surface is admin-facing today; the
268
+ and verify which lake (raw vs tokenized) actually fed the model. The audit surface is admin-facing today; the
264
269
  guarantees it captures back-stop Layer 1.
265
270
 
266
271
  ## Consumer takeaways
267
272
 
268
- 1. **Default to `'unregulated'` agent `data_access`.** Opt into
269
- `'regulated'` only when the action genuinely needs raw
270
- values, and pair with a session role that's authorised.
273
+ 1. **Reach for `'tokenized'` agent `data_access` first.** The field
274
+ is required, so this is a decision you must make rather than
275
+ inherit; choose `'raw'` only when the prompt genuinely needs real
276
+ values, and pair it with a session role that is authorised. The
277
+ tier should follow what the prompt can actually reach — an agent
278
+ whose prompt carries only an account number and a type has no
279
+ identity to protect, and one reading free-text patient messages
280
+ has plenty.
271
281
 
272
282
  2. **Use only the 13 custom filters** named in Layer 2. Bodies
273
283
  that reference other filter names will silently fall through
@@ -398,14 +398,14 @@ resolves the page:
398
398
 
399
399
  ```typescript
400
400
  const { data: page } = await api.connectedApps.resolvePage(
401
- tenantSlug, slug, // tenant + connected-app slug
401
+ tenantSlug, datalakeSlug, slug, // tenant + datalake + app slug
402
402
  {
403
403
  short_path: '<the-short-path-from-the-URL>',
404
404
  user_agent: req.headers.get('user-agent') ?? '',
405
405
  },
406
406
  )
407
407
  // page.message — the rendered message row
408
- // page.message.body — the regulated rendered body (raw)
408
+ // page.message.body — the rendered body, real values (raw lake)
409
409
  // page.route_path — the route the link was minted against
410
410
  // (e.g. '/forms/review')
411
411
  // page.metadata — parsed `connected_app_metadata_template`
@@ -418,7 +418,7 @@ the customer opens or submits the form:
418
418
 
419
419
  ```typescript
420
420
  await api.connectedApps.updateMessageTracking(
421
- tenantSlug, slug,
421
+ tenantSlug, datalakeSlug, slug,
422
422
  {
423
423
  short_path: '<same-short-path>',
424
424
  opened_at: new Date().toISOString(),
@@ -27,7 +27,7 @@ import path from 'node:path'
27
27
  const LE_TEMPLATE = fs.readFileSync(
28
28
  path.join(
29
29
  process.env.COOKBOOK_FIXTURES_DIR!,
30
- 'foundation/_lead_submissions_foundation_legal_entity.liquid',
30
+ 'organic-marketing/_lead_submissions_foundation_legal_entity.liquid',
31
31
  ),
32
32
  'utf8',
33
33
  )