@alvera-ai/platform-sdk 0.17.0 → 0.18.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 (83) hide show
  1. package/.agent/AGENTS.md +82 -144
  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 +36 -34
  34. package/.agent/mock-services.md +1 -1
  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/README.md +2 -2
  41. package/dist/bin/platform-sdk.mjs +33 -47
  42. package/dist/bin/platform-sdk.mjs.map +1 -1
  43. package/dist/index.d.mts +565 -379
  44. package/dist/index.d.mts.map +1 -1
  45. package/dist/index.mjs +494 -59
  46. package/dist/index.mjs.map +1 -1
  47. package/package.json +4 -3
  48. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +0 -88
  49. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +0 -47
  50. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +0 -24
  51. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +0 -38
  52. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_compliance_screening.liquid +0 -59
  53. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_mdm.liquid +0 -36
  54. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_mdm.liquid +0 -30
  55. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_payment_account.liquid +0 -55
  56. package/.agent/cookbook/_fixtures/subscription/_customers_subscription_mdm.liquid +0 -20
  57. package/.agent/cookbook/_setup/foundation.md +0 -359
  58. package/.agent/cookbook/_setup/healthcare.md +0 -361
  59. package/.agent/cookbook/_setup/payments.md +0 -365
  60. package/.agent/cookbook/_setup/subscription.md +0 -364
  61. package/.agent/cookbook/action-status-updaters.md +0 -278
  62. package/.agent/cookbook/ai-agent-invoke.md +0 -279
  63. package/.agent/cookbook/appointment-review-sms-workflow.md +0 -801
  64. package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -696
  65. package/.agent/cookbook/bulk-ingest.md +0 -302
  66. package/.agent/cookbook/contact-us-triage-with-llm.md +0 -663
  67. package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -659
  68. package/.agent/cookbook/generic-tables.md +0 -244
  69. package/.agent/cookbook/invite-team.md +0 -200
  70. package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -661
  71. package/.agent/cookbook/marketing-campaign-send.md +0 -1044
  72. package/.agent/cookbook/paginated-restapi-poller.md +0 -383
  73. package/.agent/cookbook/rest-fetch.md +0 -273
  74. package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -773
  75. package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -665
  76. package/.agent/cookbook/system-templates.md +0 -165
  77. package/.agent/cookbook/talk-to-data.md +0 -178
  78. package/.agent/cookbook/triage-prospects-by-priority.md +0 -571
  79. package/.agent/cookbook/welcome-sms-for-customers.md +0 -647
  80. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/memorandum-of-association-01.png +0 -0
  81. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/sample_two_page.pdf +0 -0
  82. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/_customers_subscription_customer.liquid +0 -0
  83. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/stripe_customers_batch1.csv +0 -0
@@ -4,9 +4,8 @@ Templates are an **INFRASTRUCTURE** building block: the platform
4
4
  ships a library of Liquid templates that other infrastructure
5
5
  resources consume — interoperability contracts, AI agent prompts,
6
6
  workflow definitions, status poller bodies. Templates are
7
- **read-only** from the SDK; the platform composes them
8
- per-industry, and each datalake's `data_domain` selects which
9
- surface in its catalog.
7
+ **read-only** from the SDK — they ship with the platform and a
8
+ datalake's catalog is whatever the platform exposes to it.
10
9
 
11
10
  SDK namespace: `api.templates`.
12
11
 
@@ -21,14 +20,15 @@ Three discovery methods, two complementary response shapes —
21
20
  structured (for code) and markdown (for agents):
22
21
 
23
22
  ```typescript
24
- import type { SystemTemplate } from '@alvera-ai/platform-sdk'
23
+ // The systemTemplates catalog response has no exported type on the
24
+ // public surface yet — `catalog` is inferred from the call.
25
25
 
26
26
  // Structured catalog
27
27
  const { data: catalog } = await api.templates.systemTemplates(
28
28
  tenantSlug,
29
29
  datalakeSlug,
30
30
  )
31
- // catalog.data: SystemTemplate[] — the full in-scope list, NOT paginated
31
+ // catalog.data — the full in-scope list, NOT paginated
32
32
  // { path, content, output_schema }
33
33
  // path category/sub-category/name segment string
34
34
  // content Liquid template body
@@ -59,10 +59,12 @@ const { data: templateMd } = await api.templates.metadataDetails(
59
59
  Template paths follow `<category>/<sub-category>/<name>`.
60
60
  Categories the SDK exposes include
61
61
  `data_activation/interoperability/`, `ai_agents/`, `workflows/`,
62
- `status_poller/`. Categories with an industry segment (e.g.
63
- `ai_agents/<industry>/*`) filter to the datalake's `data_domain`;
64
- categories without (e.g. `status_poller/*/*`) are globals
65
- present in every catalog.
62
+ `status_poller/`. Path segments below the category are platform-owned and have
63
+ outlived at least one naming scheme, so **discover paths via
64
+ `api.templates` rather than constructing them.** The
65
+ `data_domain` filter that used to select a datalake's slice of
66
+ the catalog was removed with GH-859; what remains is
67
+ server-decided.
66
68
 
67
69
  ## 3. Lifecycle
68
70
 
@@ -84,10 +86,9 @@ trying to add to the catalog.
84
86
  `'data_activation_interoperability'`). Check the typed
85
87
  parameter for the valid set; multiple templates can share a
86
88
  basename across intents. Within a single intent, the basename
87
- must resolve uniquely under its `<intent>+<data_domain>`
88
- prefix — if two sub-directories under that prefix ship the same
89
- basename (e.g. two vendors under
90
- `data_activation/interoperability/<industry>/`),
89
+ must resolve uniquely under its `<intent>` prefix — if two
90
+ sub-directories under that prefix ship the same basename (e.g.
91
+ two vendors under `data_activation/interoperability/`),
91
92
  `metadataDetails` returns `409 Conflict` with the candidate
92
93
  paths; a missing basename returns `404`.
93
94
 
package/.agent/tools.md CHANGED
@@ -116,17 +116,27 @@ mistake with a consequence: a `rest_api` Twilio sender has no
116
116
  happened to it** — no delivery status, no reconciliation.
117
117
 
118
118
  ```typescript
119
+ import { ToolIntent } from '@alvera-ai/platform-sdk'
120
+
119
121
  await api.tools.create(tenantSlug, datalakeSlug, {
120
122
  name: 'Campaign Sender — Main Line',
121
- intent: 'sms',
122
- tool_body_type: 'twilio',
123
- variant_type: 'primary',
124
- account_sid: 'AC…', // "AC" + 32 hex
125
- auth_token: '…', // writeOnly — never comes back on a read
126
- from_number: '+15550000001', // XOR with messaging_service_sid
127
- // base_url — nullable; blank means https://api.twilio.com
128
- // timeout_ms — 1..300000, nullable
129
- // base_message — ComplexTemplateConfig
123
+ description: 'Twilio sender for the campaign workflow.',
124
+ intent: ToolIntent.SMS,
125
+ status: 'active',
126
+ datalake_id: datalakeId,
127
+ // The provider-specific fields are NESTED under `body`, discriminated by
128
+ // `tool_body_type` — they are not top-level keys on the create request.
129
+ // Flattening them is the most common 422 on this endpoint.
130
+ body: {
131
+ tool_body_type: 'twilio',
132
+ variant_type: 'primary',
133
+ account_sid: 'AC…', // "AC" + 32 hex
134
+ auth_token: '…', // writeOnly — never comes back on a read
135
+ from_number: '+15550000001', // XOR with messaging_service_sid
136
+ // base_url — nullable; blank means https://api.twilio.com
137
+ // timeout_ms — 1..300000, nullable
138
+ // base_message — ComplexTemplateConfig
139
+ },
130
140
  })
131
141
  ```
132
142
 
@@ -627,13 +637,34 @@ await api.tools.testInvocation(
627
637
  inlines the value verbatim; `type: 'system'` references a
628
638
  platform-shipped template (see `templates.md`).
629
639
 
630
- **Test-invocation accepts exactly FIVE `tool_call_type` variants** —
631
- the interactive ones. Its request casts into a `ManualToolInvocation`
632
- embed (`on_type_not_found: :raise`) whose polymorphic set is
633
- `sms_request`, `mms_request`, `email_request`, `restapi_request`,
634
- `aws_lambda_request`. A tool whose body is SFTP / S3 / SQL /
635
- SharePoint has **no** test-invocation variant and cannot be exercised
636
- through this endpoint (same shape as the `status_poller` case below).
640
+ **Test-invocation accepts SIX `tool_call_type` variants** — the
641
+ interactive ones. Its request casts into a `ManualToolInvocation` embed
642
+ (`on_type_not_found: :raise`) whose polymorphic set is `sms_request`,
643
+ `mms_request`, `email_request`, `restapi_request`, `aws_lambda_request`
644
+ and `sql_query`.
645
+
646
+ THREE LISTS LIVE HERE and conflating them is the mistake this paragraph
647
+ used to make. `tool_body_type` says what a tool IS (12 variants — `sns`,
648
+ `twilio`, `s3`, `sql_database`, `sftp`, `sharepoint`, `manual_upload`, …).
649
+ `tool_call_type` says how a CALL is shaped (9). Test-invocation accepts a
650
+ subset of the call types (6). They do not map one-to-one: `sns` and
651
+ `twilio` are both bodies that send SMS, so both call through
652
+ `sms_request`; the `sql_database` body calls through `sql_query`.
653
+
654
+ So a tool whose body is **S3, SFTP or SharePoint** has no test-invocation
655
+ variant and cannot be exercised through this endpoint (same shape as the
656
+ `status_poller` case below). SFTP and SharePoint are coming; S3 is not
657
+ currently reachable this way either.
658
+
659
+ **`manual_upload` is the exception worth knowing.** It is excluded from
660
+ test-invocation, but it is NOT unexecutable — it has a real executor
661
+ (`Platform.Tools.ManualUpload`, protocols.ex:105) which routes to the
662
+ `ingest_json` path rather than the automated fetch pipeline. There is
663
+ nothing to dial interactively because the input is a file someone
664
+ supplies; the tool still runs. A born-red contract that demands a
665
+ test-invocation smoke for a manual-upload tool is therefore asking for
666
+ something that cannot exist, and the cure is to assert the tool's
667
+ configuration and its provenance binding instead.
637
668
  Those config shapes still exist — but on the broader **workflow-action
638
669
  `ToolCallConfig`** surface (see `tool-call-configs.md`), not here. The
639
670
  per-branch required-list for the five test-invocable variants:
@@ -661,10 +692,18 @@ per-branch required-list for the five test-invocable variants:
661
692
 
662
693
  tool_call_type: 'restapi_request'
663
694
  required: path, method, pagination_context_template
664
- enums: method ∈ HTTP methods (GET, POST, PUT, PATCH, DELETE,
665
- HEAD, OPTIONS)
666
- optional: body, params (each TemplateConfig — 'identity' or
667
- 'custom' both accepted)
695
+ enums: method ∈ 'head' | 'get' | 'put' | 'post' | 'delete' |
696
+ 'patch' — LOWERCASE. 'POST' is rejected as
697
+ `/tool_call/method — Invalid value for enum`.
698
+ optional: body, params
699
+ shapes: path, body, params and pagination_context_template are
700
+ EACH a TemplateConfig ({ type, body }) — 'identity' or
701
+ 'custom' both accepted. `path` is NOT a plain string;
702
+ passing one is `/tool_call/path — Invalid value: Invalid
703
+ object. Got: string`. It is an envelope so a path can be
704
+ interpolated per row.
705
+ pagination_context_template is required even with nothing
706
+ to paginate — `{ type: 'null' }` is how you say so.
668
707
 
669
708
  tool_call_type: 'aws_lambda_request'
670
709
  required: payload (TemplateConfig — 'identity' acceptable)
@@ -714,9 +753,12 @@ that is the cheapest way to prove its credentials:
714
753
  await api.tools.testInvocation(tenantSlug, datalakeSlug, pollerId, {
715
754
  tool_call: {
716
755
  tool_call_type: 'restapi_request',
717
- method: 'get',
756
+ method: 'get' as const,
718
757
  path: { type: 'custom', body: '/v3/YOUR-DOMAIN/events' },
719
758
  params: { type: 'custom', body: '{"limit": 1}' }, // keep it read-only
759
+ // REQUIRED, even for a one-shot probe: a REST call must always say
760
+ // whether there is another page. Omit it and the create is refused.
761
+ pagination_context_template: { type: 'custom', body: '{"has_next": false}' },
720
762
  },
721
763
  })
722
764
  ```
@@ -91,26 +91,26 @@ The canonical pattern — capture both returned identifiers and use
91
91
  each for the calls it keys:
92
92
 
93
93
  ```typescript
94
- // 1. create — id AND slug are both returned in the response
94
+ // 1. create — every response carries `id`; only SOME carry `slug`.
95
95
  const { data: created } = await api.tools.create(
96
96
  tenantSlug, datalakeSlug, body,
97
97
  )
98
- const toolId = created.id // <-- CRUD + cross-resource refs
99
- const toolSlug = created.slug // <-- human handle; ops verbs on
100
- // resources that expose them
98
+ const toolId = created.id! // <-- CRUD + cross-resource refs
99
+
100
+ // A tool has NO slug, and neither does a generic table — they are
101
+ // addressed by id and by name respectively. The slug-bearing responses
102
+ // are the ones whose ops verbs take a slug in the path: datalakes,
103
+ // workflows, data activation clients, connected apps, AI agents and
104
+ // interoperability contracts. Reach for `.slug` only on those, and
105
+ // note every id and slug is optional on the wire, so a strict consumer
106
+ // needs the `!` (or a guard) that the cookbooks use.
101
107
 
102
108
  // 2a. CRUD reads/updates key on the UUID id
103
109
  await api.tools.get(tenantSlug, datalakeSlug, toolId)
104
110
 
105
- // 2b. downstream resources reference the tool by id
106
- await api.actionStatusUpdaters.create(tenantSlug, datalakeSlug, {
107
- updater_tool_id: toolId,
108
- action_log_config: {
109
- type: 'custom',
110
- body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
111
- },
112
- // …
113
- })
111
+ // 2b. downstream resources reference the tool by ID, never by name or
112
+ // slug — see action_status_updaters.md for the full body:
113
+ // updater_tool_id: toolId
114
114
  ```
115
115
 
116
116
  Never:
@@ -37,8 +37,8 @@ connected apps, AI agents) must exist before you `create()`.
37
37
 
38
38
  ```typescript
39
39
  import type {
40
- WorkflowRequestWritable,
41
- WorkflowResponse,
40
+ AgenticWorkflowRequestWritable,
41
+ AgenticWorkflowResponse,
42
42
  } from '@alvera-ai/platform-sdk'
43
43
 
44
44
  const { data: created } = await api.workflows.create(
@@ -47,12 +47,23 @@ const { data: created } = await api.workflows.create(
47
47
  {
48
48
  name: 'Appointment Review SMS',
49
49
  description: 'Send a review-request SMS after a fulfilled appointment',
50
- dataset_type: 'appointment', // the dataset this workflow listens on
50
+
51
+ // The workflow listens on ONE generic table, named by id. The
52
+ // platform's own dataset types (legal_entity, message,
53
+ // action_log, ...) are the other legal values — see §4.
54
+ dataset_type: 'generic_table',
55
+ generic_table_id: appointmentsTableId,
56
+
57
+ // Load-bearing whenever an action mints a per-subject
58
+ // `connected_app_form_url`: resolution is what attaches the row
59
+ // to its legal entity. See §2.
60
+ skip_mdm_resolution: false,
61
+
51
62
  status: 'live', // 'live' | 'draft' | 'manual' (see §5)
52
63
  tags: ['appointments', 'sms'], // REQUIRED on every write — [] if untagged (see §2)
53
64
  filter_config: {
54
65
  type: 'custom',
55
- body: `{% if appointment.source_uri == "12345.example.com" %}true{% endif %}`,
66
+ body: '{% if event_dataset.status == "fulfilled" %}true{% endif %}',
56
67
  // NO output_schema here — filter_config takes { type, body } only.
57
68
  // See §2: output_schema is server-pinned for filter_config, not a
58
69
  // request field.
@@ -65,30 +76,37 @@ const { data: created } = await api.workflows.create(
65
76
  context_datasets: [
66
77
  {
67
78
  dataset_type: 'message',
68
- where_clause: `rm.patient_id = '{{ patient_id }}' AND rm.sent_at > NOW() - INTERVAL '6 months'`,
79
+ where_clause:
80
+ `m.legal_entity_id = '{{ legal_entity_id }}' ` +
81
+ `AND m.sent_at > NOW() - INTERVAL '6 months'`,
69
82
  limit: 1,
70
83
  position: 0,
71
84
  },
72
85
  ],
73
86
  actions: [
74
87
  {
75
- action_type: 'sms',
88
+ action_type: ActionType.SMS,
76
89
  tool_id: smsToolId,
77
90
  decision_key: 'send_appointment_review_sms',
78
91
  position: 0,
79
92
  trigger_template: 'now', // 'now' = next available slot
80
93
  action_window_start: 9, // send only 09:00–20:59 in the
81
94
  action_window_end: 21, // datalake tz (SMS quiet-hours failsafe; see §2)
95
+
96
+ // Row identity for a generic_table workflow is the event
97
+ // row's own unique column. There is no {{ subject_id }} —
98
+ // see §2, and note an unknown binding renders EMPTY.
82
99
  idempotency_template:
83
- '{{ patient_id }}-{{ appointment.unregulated_appointment_id }}-{{ decision_key }}',
100
+ '{{ event_dataset.appointment_id }}-{{ decision_key }}',
101
+
84
102
  connected_app_id: connectedAppId,
85
103
  connected_app_route: '/forms/review',
86
104
  connected_app_metadata_template:
87
- '{"appointment_id":"{{ appointment.unregulated_appointment_id }}"}',
105
+ '{"appointment_id":"{{ event_dataset.appointment_id }}"}',
88
106
  tool_call: {
89
107
  tool_call_type: 'sms_request',
90
- to: { type: 'custom', body: '{{ patient_phone }}' },
91
- body: { type: 'custom', body: 'Hi {{ patient_first_name }}, please share your feedback at {{ connected_app_form_url }}' },
108
+ to: { type: 'custom', body: '{{ event_dataset.patient_mobile | e164 }}' },
109
+ body: { type: 'custom', body: 'Hi {{ event_dataset.patient_name | split: " " | first }}, please share your feedback at {{ connected_app_form_url }}' },
92
110
  sms_type: 'transactional',
93
111
  },
94
112
  },
@@ -176,13 +194,21 @@ dataset row; the platform deduplicates so the same idempotency
176
194
  key never fires twice. Typical recipe:
177
195
 
178
196
  ```
179
- '{{ <subject_id> }}-{{ <event_id> }}-{{ decision_key }}'
197
+ '{{ <row identity> }}-{{ action_id }}-{{ decision_key }}'
180
198
  ```
181
199
 
182
- The placeholder names depend on the workflow's `dataset_type`
183
- (patient_id + appointment.unregulated_appointment_id for an
184
- appointment-driven workflow, customer_id + invoice.id for an
185
- invoice-driven workflow, etc.).
200
+ The row identity depends on the workflow's `dataset_type`. For a
201
+ `generic_table` workflow it is the event row's unique column —
202
+ `{{ event_dataset.submission_id }}`, `{{ event_dataset.customer_number }}`;
203
+ for a `legal_entity` workflow the row IS the subject, so it is
204
+ `{{ legal_entity.id }}`.
205
+
206
+ **There is no `{{ subject_id }}`.** GH-859 removed it along with the
207
+ per-domain subjects (`patient_id`, `customer_id`, `party_id`), and it is a
208
+ trap rather than merely a rename: an unknown binding renders EMPTY instead of
209
+ failing, so a template still using one produces a key that is missing the very
210
+ component it exists to key on — and nothing anywhere says so. If two rows that
211
+ should dedupe are firing twice, look here first.
186
212
 
187
213
  ### Two scheduling levers, and they answer different questions
188
214
 
@@ -358,9 +384,10 @@ are removed, and re-supplied ones are repositioned.
358
384
  ```typescript
359
385
  await api.workflows.create(tenantSlug, datalakeSlug, {
360
386
  name: 'Contact-Us Triage',
361
- dataset_type: 'generic_table',
387
+ description: 'Triages inbound contact-us messages.',
388
+ dataset_type: 'generic_table' as const,
389
+ status: 'live' as const,
362
390
  tags: [],
363
- // …other workflow fields…
364
391
  workflow_ai_agents: [
365
392
  {
366
393
  ai_agent_id: aiAgentId, // UUID from api.aiAgents.create
@@ -402,7 +429,7 @@ variable. Field paths match the columns of the workflow's
402
429
  `dataset_type`:
403
430
 
404
431
  ```
405
- dataset_type: 'appointment' → event_dataset.id, event_dataset.start, ...
432
+ dataset_type: 'legal_entity' → event_dataset.id, event_dataset.name, ...
406
433
  dataset_type: 'generic_table' → event_dataset.<column_name> for each
407
434
  column on the bound table
408
435
  ```
@@ -534,7 +561,7 @@ for a time, or at `scheduled_at` if you did.
534
561
  const { data: run } = await api.workflows.run(
535
562
  tenantSlug, datalakeSlug, workflowSlug,
536
563
  {
537
- sql_where_clause: `ra.id = '${appointmentId}'`, // run against the regulated tables
564
+ sql_where_clause: `le.id = '${legalEntityId}'`, // fragment against the dataset's alias
538
565
  mode: 'live', // 'live' | 'dry_run'
539
566
  manual_override: false, // see below
540
567
  scheduled_at: '2026-08-12T09:00:00+01:00', // OPTIONAL — see below
@@ -616,11 +643,17 @@ worker — both sides contend on one conditional update, so exactly one wins
616
643
  and the loser is told. Treat a refusal as a normal outcome to surface, not
617
644
  an error to retry.
618
645
 
619
- `sql_where_clause` accepts a fragment against the dataset's
620
- regulated tables (`ra` for appointments, `rp` for patients,
621
- etc. — same alias scheme as `data_activation_clients.md` §6.5).
622
- The id column is ambiguous across joins; always prefix
623
- (`ra.id`, `rp.id`).
646
+ `sql_where_clause` accepts a fragment against the dataset's own
647
+ tables. For a **platform dataset** the server assigns the alias
648
+ (`le` legal entities, `m` messages, `al` action logs, `d` documents,
649
+ `bo` beneficial owners — same scheme as `data_activation_clients.md`
650
+ §6.5), and `id` is ambiguous across the base query's joins, so always
651
+ prefix it: `le.id`.
652
+
653
+ For a **generic-table** workflow there is no alias — the run selects
654
+ on the table's own columns directly, as the cookbooks do
655
+ (`customer_number IN (…)`). Prefixing one is an error, not a
656
+ precaution.
624
657
 
625
658
  `manual_override: true` bypasses **dedupe/idempotency ONLY** —
626
659
  the same row may fire again even though its idempotency tuple
@@ -647,9 +680,9 @@ context build + AI enrichment**, and the action's own
647
680
 
648
681
  ```typescript
649
682
  const { data: result } = await api.workflows.execute(
650
- tenantSlug, workflowSlug,
683
+ tenantSlug, datalakeSlug, workflowSlug,
651
684
  {
652
- dataset_id: unregulatedAppointmentId, // UNREGULATED id (not regulated)
685
+ dataset_id: appointmentRowId, // the row's id — see below
653
686
  decision_key: 'send_appointment_review_sms',
654
687
  mode: 'live', // 'live' | 'dry_run' (same enum as .run)
655
688
  manual_override: true, // load-bearing on re-fires
@@ -666,11 +699,22 @@ const { data: result } = await api.workflows.execute(
666
699
  // result.scheduled_job_id — background job id (null when count 0)
667
700
  ```
668
701
 
669
- Note that `dataset_id` is the **unregulated** id, not the
670
- regulated one. The unregulated tier is what the workflow
671
- runtime reads its action context from; passing the regulated
672
- id will fail to resolve. Issue a `dataAccessMode: 'unregulated'`
673
- search to obtain the right id.
702
+ `dataset_id` is simply the row's id, and **there is only one.** An
703
+ older version of this guide said to pass a tokenized-tier id and
704
+ warned that the raw one would not resolve. That was true of a design
705
+ with paired schemas and two rows per subject; GH-859 removed it.
706
+ Today the derived lakes are replicated copies that preserve the primary
707
+ key, so every lake reports the same id. And the lookup does not consult
708
+ a mode at all: `validate_dataset_exists/4` passes `:raw` as a **literal**
709
+ in both of its clauses — there is no branch, no mode argument threaded
710
+ in, and no third clause. Every dataset type resolves against the raw
711
+ lake.
712
+
713
+ So an id from a tokenized or redacted search is already the id
714
+ `.execute` wants. The re-search the old advice called for was not
715
+ merely unnecessary; it was answering a question the code never asks.
716
+ `primary-care` §029 reads one row through all three lakes on a single
717
+ slug, which is the direct evidence that the key survives replication.
674
718
 
675
719
  `manual_override: true` on `.execute` sets the unique-key
676
720
  constraint to `false` on the per-row job, allowing re-fires
@@ -745,16 +789,14 @@ Each log carries:
745
789
  - `error_message` — coarse failure summary (e.g.
746
790
  `"AI enrichment failed: <agent_name>"`); rich detail lives
747
791
  in `error.json` (§ Per-step artifacts below)
748
- - `action_execution_logs` — nested rows, one per action that fired
749
- (`message_body` virtual field is rendered when read via the
750
- `dataAccessMode: 'regulated'` mode)
792
+ - `action_execution_logs` — nested rows, one per action that fired,
793
+ carrying the `message_body` virtual field
751
794
 
752
795
  Methods:
753
796
 
754
797
  ```
755
798
  .list(tenantSlug, datalakeSlug, workflowSlug, query?) Paged
756
- .get(tenantSlug, datalakeSlug, workflowSlug, logId,
757
- { dataAccessMode? }) One row
799
+ .get(tenantSlug, datalakeSlug, workflowSlug, logId) One row
758
800
  .download(tenantSlug, datalakeSlug, workflowSlug, logId) The run's artifact
759
801
  ```
760
802
 
@@ -774,17 +816,20 @@ await api.workflows.workflowLogs.list(tenantSlug, datalakeSlug, slug, {
774
816
  `filters` uses the Flop empty-bracket serialization on the wire —
775
817
  the SDK's serializer handles it; never hand-roll the query string.
776
818
 
777
- The `dataAccessMode` query parameter on `.get` controls how
778
- `message_body` is rendered:
779
- - `'regulated'` — raw rendered body (with the resolved short
780
- path `/t/<token>`)
781
- - `'unregulated'` — tokenised display body
819
+ `.get` takes NO `dataAccessMode`. It used to; the platform removed the
820
+ `data_access_mode` query from this endpoint, and the generated type is now
821
+ `query?: never`, so passing one no longer compiles.
822
+
823
+ An execution log is workflow bookkeeping rather than lake rows, so there is
824
+ no tier to select. Choosing a tier belongs to the reads that return data —
825
+ `datasets.search` and `datalakes.executeSql` both take a mode, and that is
826
+ where `'raw' | 'tokenized' | 'redacted'` still applies.
782
827
 
783
828
  ### Per-step artifacts (event, filter, enrichment, error)
784
829
 
785
- For each workflow execution, the platform writes JSON artifacts
786
- to the datalake's regulated cloud-storage bucket. The keys
787
- follow a fixed convention:
830
+ For each workflow execution, the platform writes JSON artifacts to
831
+ the datalake's cloud-storage bucket. The keys follow a fixed
832
+ convention:
788
833
 
789
834
  ```
790
835
  workflows/<workflow_id>/executions/<execution_log_id>/<step>.json
@@ -798,7 +843,7 @@ Fetch them via the datalake's `createDownloadLink`:
798
843
  ```typescript
799
844
  const { data: link } = await api.datalakes.createDownloadLink(
800
845
  tenantSlug, datalakeSlug,
801
- { bucket: regulatedBucket, key: 'workflows/.../filter.json' },
846
+ { bucket: datalakeBucket, key: 'workflows/.../filter.json' },
802
847
  )
803
848
  const artifact = await (await fetch(link.url)).json()
804
849
  ```
@@ -984,15 +1029,16 @@ row doesn't swallow it. When counting "what actually went out", filter
984
1029
  (Nested `workflow_ai_agents` follow the same whole-array-replace
985
1030
  rule — re-supply the full set on every `update()`; see §3.)
986
1031
 
987
- 3. **`sql_where_clause` ids require an alias prefix.** `id` is
988
- ambiguous across the multi-JOIN base query; use
989
- `<base_alias>.id` (`ra.id`, `rp.id`, `rc.id`, etc.).
1032
+ 3. **`sql_where_clause` ids require an alias prefix — on a platform
1033
+ dataset.** `id` is ambiguous across the multi-JOIN base query, so
1034
+ use `<base_alias>.id` (`le.id`, `m.id`). A generic-table workflow
1035
+ has no alias and takes bare column names.
990
1036
 
991
- 4. **`.execute` takes the UNREGULATED dataset id, `.run` takes
992
- the REGULATED id.** This asymmetry traps consumers who
993
- reuse the id they got from a regulated-mode search. Issue
994
- a fresh search with `dataAccessMode: 'unregulated'` before
995
- calling `.execute`.
1037
+ 4. **`.execute` and `.run` take the same id.** They once did not, and
1038
+ an older version of this guide described the asymmetry at length.
1039
+ The paired schemas that caused it are gone: the derived lakes
1040
+ preserve the primary key, so there is one id per row and no
1041
+ re-search is needed before calling `.execute`.
996
1042
 
997
1043
  5. **`manual_override: true` on `.execute` is required for
998
1044
  re-fires.** Without it, the per-row job dedupes against the
package/README.md CHANGED
@@ -39,8 +39,8 @@ const api: PlatformApi = createIsolatedPlatformApi({
39
39
  // 3. call resources — every nested resource is tenant + datalake scoped
40
40
  const { data: datalake } = await api.datalakes.create(tenantSlug, {
41
41
  name: 'Production Lake',
42
- data_domain: 'subscription',
43
- // …database + storage credentials…
42
+ type: 'raw',
43
+ // …db_reader_* / db_writer_* + cloud_storage credentials…
44
44
  })
45
45
 
46
46
  const { data, meta } = await api.tools.list(tenantSlug, datalake.slug)
@@ -28,53 +28,39 @@ function buildManagedBlock() {
28
28
  "composing API calls for a scenario from primitives, start from the",
29
29
  "closest recipe and diverge only with a stated reason.",
30
30
  "",
31
- "### Recipes by use case",
32
- "",
33
- "**Healthcare**",
34
- "",
35
- `- \`${corpus}/cookbook/appointment-review-sms-workflow.md\` — send a`,
36
- " patient review-request SMS after a fulfilled appointment,",
37
- " deep-linked to a connected-app feedback form.",
38
- `- \`${corpus}/cookbook/contact-us-triage-with-llm.md\` — triage inbound`,
39
- " contact-us messages into three priority buckets (appointment /",
40
- " job-application / spam) via an LLM agent, route each to a tailored",
41
- " SMS action.",
42
- "",
43
- "**Subscription**",
44
- "",
45
- `- \`${corpus}/cookbook/welcome-sms-for-customers.md\` — send a welcome`,
46
- " SMS to newly contracted customers with a self-serve billing-portal",
47
- " link.",
48
- `- \`${corpus}/cookbook/dunning-sms-for-delinquent.md\` — send a`,
49
- " payment-reminder SMS to delinquent customers (filtered on",
50
- " phone-on-file and tax-id-verified) with a pay-invoice link.",
51
- `- \`${corpus}/cookbook/triage-prospects-by-priority.md\` — triage`,
52
- " inbound AR customers into priority bands (high / medium / low) via",
53
- " an LLM agent, route each band to a tailored SMS action.",
54
- "",
55
- "**Payments**",
56
- "",
57
- `- \`${corpus}/cookbook/kyc-notification-on-account-activation.md\` —`,
58
- " send a KYC-notification SMS when a payment account transitions to",
59
- " active status.",
60
- `- \`${corpus}/cookbook/sanctions-screening-with-agent-review.md\` —`,
61
- " disambiguate gray-zone sanctions screenings via an LLM agent, route",
62
- " confirmed-clean and confirmed-block outcomes to distinct SMS",
63
- " actions.",
64
- "",
65
- "**Foundation**",
66
- "",
67
- `- \`${corpus}/cookbook/birthday-greeting-sms-trigger.md\` — send a`,
68
- " happy-birthday SMS on each contact's next birthday using a",
69
- " pure-Liquid trigger (year-roll math).",
70
- `- \`${corpus}/cookbook/score-leads-with-llm-categorization.md\` — score`,
71
- " inbound leads into four bands (hot / warm / cold / spam) via an LLM",
72
- " agent, route each band to a tailored SMS action.",
73
- `- \`${corpus}/cookbook/marketing-campaign-send.md\` — send an A/B`,
74
- " marketing campaign across SMS and email: the suppression and",
75
- " reachability gates and the A/B split all live in the workflow, each",
76
- " send carries a per-recipient short link, and the reply re-attaches",
77
- " to the customer who sent it.",
31
+ "### The four cookbooks",
32
+ "",
33
+ "One per use case. Each stands up its own tenant, datalake and",
34
+ "resources and shares nothing with the others — read one end to end",
35
+ "and you need no second file. Duplication between them is deliberate.",
36
+ "",
37
+ `- \`${corpus}/cookbook/organic-marketing.md\` — score inbound leads`,
38
+ " into four bands with an LLM agent, schedule a birthday greeting a",
39
+ " year out with a pure-Liquid trigger, and run an A/B campaign whose",
40
+ " suppression gates and split live in the workflow. Ends with",
41
+ " delivery reconciliation and a natural-language read of the lake.",
42
+ " One raw datalake.",
43
+ `- \`${corpus}/cookbook/payments-compliance.md\` — disambiguate`,
44
+ " gray-zone sanctions screenings with an LLM agent that never sees",
45
+ " who was screened, and fire a KYC notification on account",
46
+ " activation. Ends by reading one row through raw, tokenized and",
47
+ " redacted to show what each reader gets. Three datalakes.",
48
+ `- \`${corpus}/cookbook/subscription-saas.md\` — one customers table`,
49
+ " and three workflows over it: a welcome SMS gated on reachability,",
50
+ " a dunning reminder gated on reachability AND KYC, and an LLM agent",
51
+ " banding accounts by priority. Then the same table filled three",
52
+ " more ways — inline, bulk CSV, REST fetch — and a teammate invited",
53
+ " onto the tenant. One raw datalake.",
54
+ `- \`${corpus}/cookbook/primary-care.md\` — a review request that only`,
55
+ " reaches patients whose visit actually happened, an LLM agent",
56
+ " triaging inbound messages off the tokenized projection, and a",
57
+ " vision agent reading a scanned document. Three datalakes.",
58
+ "",
59
+ "**Capabilities are inside the cookbooks, not beside them.** Bulk",
60
+ "upload, REST fetch, team invitations, action status updaters,",
61
+ "paginated pollers, text-to-SQL and agent file-invocation each live in",
62
+ "the cookbook whose use case motivates them, as numbered steps. There",
63
+ "is no separate per-capability file to consult.",
78
64
  "",
79
65
  "### Top-3 consumer gotchas",
80
66
  "",