@alvera-ai/platform-sdk 0.13.0 → 0.15.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 (32) hide show
  1. package/.agent/account_management.md +31 -0
  2. package/.agent/action_status_updaters.md +273 -25
  3. package/.agent/ai_sandbox.md +4 -2
  4. package/.agent/connected_apps.md +7 -2
  5. package/.agent/cookbook/action-status-updaters.md +73 -14
  6. package/.agent/cookbook/ai-agent-invoke.md +36 -0
  7. package/.agent/cookbook/appointment-review-sms-workflow.md +32 -0
  8. package/.agent/cookbook/birthday-greeting-sms-trigger.md +35 -1
  9. package/.agent/cookbook/bulk-ingest.md +48 -0
  10. package/.agent/cookbook/contact-us-triage-with-llm.md +36 -1
  11. package/.agent/cookbook/dunning-sms-for-delinquent.md +33 -1
  12. package/.agent/cookbook/generic-tables.md +40 -0
  13. package/.agent/cookbook/kyc-notification-on-account-activation.md +35 -1
  14. package/.agent/cookbook/marketing-campaign-send.md +35 -0
  15. package/.agent/cookbook/paginated-restapi-poller.md +383 -0
  16. package/.agent/cookbook/rest-fetch.md +27 -0
  17. package/.agent/cookbook/sanctions-screening-with-agent-review.md +35 -1
  18. package/.agent/cookbook/score-leads-with-llm-categorization.md +36 -1
  19. package/.agent/cookbook/system-templates.md +36 -0
  20. package/.agent/cookbook/talk-to-data.md +39 -0
  21. package/.agent/cookbook/triage-prospects-by-priority.md +33 -1
  22. package/.agent/cookbook/welcome-sms-for-customers.md +33 -1
  23. package/.agent/datalakes.md +68 -3
  24. package/.agent/interoperability_contracts.md +29 -0
  25. package/.agent/tool-call-configs.md +11 -0
  26. package/.agent/tools.md +103 -36
  27. package/.agent/type_naming.md +4 -0
  28. package/dist/index.d.mts +189 -19
  29. package/dist/index.d.mts.map +1 -1
  30. package/dist/index.mjs +3 -1
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +2 -2
@@ -675,6 +675,38 @@ if (!tracked.message?.opened_at || !tracked.message?.form_submitted_at) {
675
675
  }
676
676
  ```
677
677
 
678
+ ## 016 — write the integration test
679
+
680
+ End the build with a test you keep: re-read the workflow and prove the
681
+ pipeline still executes — without a side effect. `mode: 'dry_run'` with a
682
+ never-matching selection runs the FULL pipeline (selection → filter →
683
+ decision) and intercepts only the final action call, so no message
684
+ leaves, yet the acknowledgement proves the workflow is runnable. This
685
+ block runs live under `make validate-cookbook`.
686
+
687
+ ```typescript
688
+ // Re-GET — the workflow must still be live, or nothing will run.
689
+ const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
690
+ if (wfRow.status !== 'live') {
691
+ throw new Error(`workflow regressed from live: ${wfRow.status}`)
692
+ }
693
+ // Behavioural probe — a dry run against a selection no row can match:
694
+ // the pipeline executes end-to-end, the final action call is
695
+ // intercepted, and the acknowledgement carries the run-log id.
696
+ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
697
+ sql_where_clause: "ra.batch_id = 'test-never-matching-batch'",
698
+ mode: 'dry_run',
699
+ manual_override: false,
700
+ })
701
+ if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
702
+ throw new Error('dry-run probe returned no workflow_run_log_id')
703
+ }
704
+ ```
705
+
706
+ If the probe fails in production, escalate with the run response as
707
+ evidence — don't flip the workflow's status or rewrite its configs to
708
+ chase the error.
709
+
678
710
  # Branches
679
711
 
680
712
  - **The cancelled appointment is filtered, not failed** — §012
@@ -175,7 +175,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
175
175
  position: 0,
176
176
  trigger_template: TRIGGER_TEMPLATE,
177
177
  idempotency_template:
178
- '{{ subject_id }}-{{ workflow_id }}-{{ action_id }}-{{ "" | uuid }}',
178
+ '{{ subject_id }}-{{ workflow_id }}-{{ action_id }}',
179
179
  connected_app_id: connectedAppId,
180
180
  connected_app_route: '/forms/birthday-greeting',
181
181
  connected_app_metadata_template:
@@ -577,6 +577,40 @@ if (!tracked.message?.opened_at || !tracked.message?.form_submitted_at) {
577
577
  }
578
578
  ```
579
579
 
580
+ ## 015 — write the integration test
581
+
582
+ End the build with a test you keep: re-read the workflow and prove the
583
+ pipeline still executes — without a side effect. `mode: 'dry_run'` with a
584
+ never-matching selection runs the FULL pipeline (selection → filter →
585
+ decision) and intercepts only the final action call, so no message
586
+ leaves, yet the acknowledgement proves the workflow is runnable. This
587
+ block runs live under `make validate-cookbook`.
588
+
589
+ ```typescript
590
+ // Re-GET — the workflow must still be live, or nothing will run.
591
+ const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
592
+ if (wfRow.status !== 'live') {
593
+ throw new Error(`workflow regressed from live: ${wfRow.status}`)
594
+ }
595
+ // Behavioural probe — a dry run against a selection no row can match:
596
+ // the pipeline executes end-to-end, the final action call is
597
+ // intercepted, and the acknowledgement carries the run-log id. The
598
+ // clause must speak this workflow's selection dialect — the dataset
599
+ // alias is `rle` here, the same alias the live run above uses.
600
+ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
601
+ sql_where_clause: "rle.batch_id = 'test-never-matching-batch'",
602
+ mode: 'dry_run',
603
+ manual_override: false,
604
+ })
605
+ if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
606
+ throw new Error('dry-run probe returned no workflow_run_log_id')
607
+ }
608
+ ```
609
+
610
+ If the probe fails in production, escalate with the run response as
611
+ evidence — don't flip the workflow's status or rewrite its configs to
612
+ chase the error.
613
+
580
614
  # Branches
581
615
 
582
616
  - **The no-DoB row is filtered, not failed** — §011 asserts the no-DoB
@@ -229,6 +229,54 @@ if (rows.length < 4) {
229
229
  }
230
230
  ```
231
231
 
232
+ ## 009 — write the integration test
233
+
234
+ End the build with a test you keep: run the bulk path in miniature —
235
+ mint a link, PUT a tiny inline CSV, enqueue it — and assert each call's
236
+ own acknowledgement. The probe rows are `test-`-prefixed so they are
237
+ unmistakably synthetic wherever they surface, and the test asserts the
238
+ enqueue ack (`job_id`), never synchronous merge completion — the worker
239
+ owns that. This block runs live under `make validate-cookbook`.
240
+
241
+ ```typescript
242
+ // Re-GET — the client must still be there and carry its ingest slug.
243
+ const { data: dacRow } = await api.dataActivationClients.get(tenantSlug, datalakeSlug, dacId)
244
+ if (dacRow.slug !== ctx.dacSlug) {
245
+ throw new Error(`DAC slug drifted on read-back: ${dacRow.slug}`)
246
+ }
247
+ // Behavioural probe — the three-step bulk path, each step asserted on
248
+ // its own response.
249
+ const probeCsv =
250
+ 'customer_number,customer_type,status,name,email,currency\n' +
251
+ `test-PROBE-1-${runSuffix},individual,contracted,test-Grace Hopper,test-grace-${runSuffix}@example.com,USD\n`
252
+ const { data: probeLink } = await api.datalakes.createUploadLink(tenantSlug, datalakeSlug, {
253
+ content_type: 'text/csv',
254
+ filename: `test-bulk-probe-${runSuffix}.csv`,
255
+ })
256
+ if (!probeLink.url || !probeLink.key) {
257
+ throw new Error('createUploadLink returned no url/key')
258
+ }
259
+ const probePut = await fetch(probeLink.url, {
260
+ method: 'PUT',
261
+ headers: { 'Content-Type': 'text/csv' },
262
+ body: probeCsv,
263
+ })
264
+ if (probePut.status !== 200) {
265
+ throw new Error(`presigned PUT failed: ${probePut.status}`)
266
+ }
267
+ const { data: probeJob } = await api.dataActivationClients.ingestFile(tenantSlug, datalakeSlug, ctx.dacSlug, {
268
+ key: probeLink.key,
269
+ })
270
+ // job_id is a NUMBER on the wire (an Oban job id), status "scheduled".
271
+ if (probeJob.job_id === undefined || probeJob.job_id === null) {
272
+ throw new Error('ingestFile probe returned no job_id')
273
+ }
274
+ ```
275
+
276
+ If the probe fails in production, escalate with the failing call's
277
+ response — don't retry the enqueue in a loop or reach into the worker's
278
+ storage to "help it along".
279
+
232
280
  # Gotchas
233
281
 
234
282
  - **The file PUT is raw HTTP, not the SDK.** `createUploadLink` and `ingestFile`
@@ -326,7 +326,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
326
326
  tool_id: toolId,
327
327
  position: 0,
328
328
  trigger_template: 'now',
329
- idempotency_template: `{{ subject_id }}-{{ action_id }}-${bucket}-{{ "" | uuid }}`,
329
+ idempotency_template: `{{ subject_id }}-{{ action_id }}-${bucket}`,
330
330
  tool_call: {
331
331
  tool_call_type: 'sms_request',
332
332
  to: { type: 'custom', body: '+15551234567' },
@@ -543,6 +543,41 @@ for (const wel of ourWels) {
543
543
  }
544
544
  ```
545
545
 
546
+ ## 011 — write the integration test
547
+
548
+ End the build with a test you keep: re-read the workflow and prove the
549
+ pipeline still executes — without a side effect. `mode: 'dry_run'` with a
550
+ never-matching selection runs the FULL pipeline (selection → filter →
551
+ decision) and intercepts only the final action call, so no message
552
+ leaves, yet the acknowledgement proves the workflow is runnable. This
553
+ block runs live under `make validate-cookbook`.
554
+
555
+ ```typescript
556
+ // Re-GET — the workflow must still be live, or nothing will run.
557
+ const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
558
+ if (wfRow.status !== 'live') {
559
+ throw new Error(`workflow regressed from live: ${wfRow.status}`)
560
+ }
561
+ // Behavioural probe — a dry run against a selection no row can match:
562
+ // the pipeline executes end-to-end, the final action call is
563
+ // intercepted, and the acknowledgement carries the run-log id. The
564
+ // clause speaks this workflow's selection dialect: a GENERIC-TABLE
565
+ // dataset is addressed by its own columns (no `ra.` dataset alias —
566
+ // that alias exists only for system-dataset selections).
567
+ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
568
+ sql_where_clause: "submission_id = 'test-never-matching-submission'",
569
+ mode: 'dry_run',
570
+ manual_override: false,
571
+ })
572
+ if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
573
+ throw new Error('dry-run probe returned no workflow_run_log_id')
574
+ }
575
+ ```
576
+
577
+ If the probe fails in production, escalate with the run response as
578
+ evidence — don't flip the workflow's status or rewrite its configs to
579
+ chase the error.
580
+
546
581
  # Branches
547
582
 
548
583
  - **The filter is permissive** — `filter_config.body: 'true'`
@@ -189,7 +189,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
189
189
  decision_key: DECISION_KEY,
190
190
  position: 0,
191
191
  trigger_template: 'now',
192
- idempotency_template: '{{ customer_id }}-{{ decision_key }}-{{ "" | uuid }}',
192
+ idempotency_template: '{{ customer_id }}-{{ decision_key }}',
193
193
  connected_app_id: connectedAppId,
194
194
  connected_app_route: '/portal/pay',
195
195
  connected_app_metadata_template:
@@ -533,6 +533,38 @@ if (!tracked.message?.opened_at || !tracked.message?.form_submitted_at) {
533
533
  }
534
534
  ```
535
535
 
536
+ ## 014 — write the integration test
537
+
538
+ End the build with a test you keep: re-read the workflow and prove the
539
+ pipeline still executes — without a side effect. `mode: 'dry_run'` with a
540
+ never-matching selection runs the FULL pipeline (selection → filter →
541
+ decision) and intercepts only the final action call, so no message
542
+ leaves, yet the acknowledgement proves the workflow is runnable. This
543
+ block runs live under `make validate-cookbook`.
544
+
545
+ ```typescript
546
+ // Re-GET — the workflow must still be live, or nothing will run.
547
+ const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
548
+ if (wfRow.status !== 'live') {
549
+ throw new Error(`workflow regressed from live: ${wfRow.status}`)
550
+ }
551
+ // Behavioural probe — a dry run against a selection no row can match:
552
+ // the pipeline executes end-to-end, the final action call is
553
+ // intercepted, and the acknowledgement carries the run-log id.
554
+ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
555
+ sql_where_clause: "ra.batch_id = 'test-never-matching-batch'",
556
+ mode: 'dry_run',
557
+ manual_override: false,
558
+ })
559
+ if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
560
+ throw new Error('dry-run probe returned no workflow_run_log_id')
561
+ }
562
+ ```
563
+
564
+ If the probe fails in production, escalate with the run response as
565
+ evidence — don't flip the workflow's status or rewrite its configs to
566
+ chase the error.
567
+
536
568
  # Branches
537
569
 
538
570
  - **The unverified customer is filtered, not failed** — §011
@@ -175,6 +175,46 @@ if (!row || row.submission_id !== ctx.submissionId) {
175
175
  }
176
176
  ```
177
177
 
178
+ ## 007 — write the integration test
179
+
180
+ End the build with a test you keep: re-read the table and prove the two
181
+ facts every consumer of it depends on — the deploy completed (with the
182
+ server-derived physical name), and the ingest path accepts a row. The
183
+ probe row is `test-`-prefixed so it is unmistakably synthetic wherever it
184
+ surfaces. This block runs live under `make validate-cookbook`.
185
+
186
+ ```typescript
187
+ // Re-GET — deployed, with the server-derived alvera_custom_ name.
188
+ const { data: tableRow } = await api.genericTables.get(tenantSlug, datalakeSlug, genericTableId)
189
+ if (tableRow.status !== 'deployed') {
190
+ throw new Error(`generic table regressed from deployed: ${tableRow.status}`)
191
+ }
192
+ if (tableRow.name !== ctx.tableName) {
193
+ throw new Error(`physical name drifted on read-back: ${tableRow.name}`)
194
+ }
195
+ // Behavioural probe — one synthetic row through the auto-provisioned
196
+ // default client; ingest is async (202), so assert the batch
197
+ // acknowledgement, never synchronous row completion.
198
+ const { data: probeAck } = await api.dataActivationClients.ingest(
199
+ tenantSlug, datalakeSlug, ctx.defaultDacSlug,
200
+ {
201
+ data: {
202
+ submission_id: `test-CDS-probe-${runSuffix}`,
203
+ customer_name: 'test-Ada Lovelace',
204
+ email: 'test-ada@example.test',
205
+ message: 'test: integration-test probe row.',
206
+ source_channel: 'portal',
207
+ },
208
+ },
209
+ )
210
+ if (typeof probeAck.batch_id !== 'string' || probeAck.batch_id.length === 0) {
211
+ throw new Error('default-client ingest did not enqueue a batch')
212
+ }
213
+ ```
214
+
215
+ If the probe fails in production, escalate with the failing response —
216
+ don't re-create the table or hand-edit the physical schema.
217
+
178
218
  # Gotchas
179
219
 
180
220
  - **`privacy_requirement` is load-bearing.** It drives the regulated/unregulated
@@ -188,7 +188,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
188
188
  decision_key: DECISION_KEY,
189
189
  position: 0,
190
190
  trigger_template: 'now',
191
- idempotency_template: '{{ payment_account.id }}-{{ decision_key }}-{{ "" | uuid }}',
191
+ idempotency_template: '{{ payment_account.id }}-{{ decision_key }}',
192
192
  connected_app_id: connectedAppId,
193
193
  connected_app_route: '/portal/kyc',
194
194
  connected_app_metadata_template: '{"payment_account_id":"{{ payment_account.id }}"}',
@@ -531,6 +531,40 @@ if (!tracked.message?.opened_at || !tracked.message?.form_submitted_at) {
531
531
  }
532
532
  ```
533
533
 
534
+ ## 014 — write the integration test
535
+
536
+ End the build with a test you keep: re-read the workflow and prove the
537
+ pipeline still executes — without a side effect. `mode: 'dry_run'` with a
538
+ never-matching selection runs the FULL pipeline (selection → filter →
539
+ decision) and intercepts only the final action call, so no message
540
+ leaves, yet the acknowledgement proves the workflow is runnable. This
541
+ block runs live under `make validate-cookbook`.
542
+
543
+ ```typescript
544
+ // Re-GET — the workflow must still be live, or nothing will run.
545
+ const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
546
+ if (wfRow.status !== 'live') {
547
+ throw new Error(`workflow regressed from live: ${wfRow.status}`)
548
+ }
549
+ // Behavioural probe — a dry run against a selection no row can match:
550
+ // the pipeline executes end-to-end, the final action call is
551
+ // intercepted, and the acknowledgement carries the run-log id. The
552
+ // clause must speak this workflow's selection dialect — the dataset
553
+ // alias is `rpa` here, the same alias the live run above uses.
554
+ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
555
+ sql_where_clause: "rpa.batch_id = 'test-never-matching-batch'",
556
+ mode: 'dry_run',
557
+ manual_override: false,
558
+ })
559
+ if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
560
+ throw new Error('dry-run probe returned no workflow_run_log_id')
561
+ }
562
+ ```
563
+
564
+ If the probe fails in production, escalate with the run response as
565
+ evidence — don't flip the workflow's status or rewrite its configs to
566
+ chase the error.
567
+
534
568
  # Branches
535
569
 
536
570
  - **The suspended account is filtered, not failed** — §011
@@ -918,6 +918,41 @@ while (Date.now() < flagDeadline) {
918
918
  if (!flagged) throw new Error('the re-ingest did not flag the reply potential_duplicate within 120s')
919
919
  ```
920
920
 
921
+ ## 013 — write the integration test
922
+
923
+ End the build with a test you keep: re-read the workflow and prove the
924
+ pipeline still executes — without a side effect. `mode: 'dry_run'` with a
925
+ never-matching selection runs the FULL pipeline (selection → filter →
926
+ decision) and intercepts only the final action call, so no message
927
+ leaves, yet the acknowledgement proves the workflow is runnable. This
928
+ block runs live under `make validate-cookbook`.
929
+
930
+ ```typescript
931
+ // Re-GET — the workflow must still be live, or nothing will run.
932
+ const { data: wfRow } = await api.workflows.get(tenantSlug, datalakeSlug, workflowId)
933
+ if (wfRow.status !== 'live') {
934
+ throw new Error(`workflow regressed from live: ${wfRow.status}`)
935
+ }
936
+ // Behavioural probe — a dry run against a selection no row can match:
937
+ // the pipeline executes end-to-end, the final action call is
938
+ // intercepted, and the acknowledgement carries the run-log id. The
939
+ // clause speaks this workflow's selection dialect: a GENERIC-TABLE
940
+ // audience is addressed by its own columns (no `ra.` dataset alias —
941
+ // that alias exists only for system-dataset selections).
942
+ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
943
+ sql_where_clause: "end_customer_id = 'test-never-matching-customer'",
944
+ mode: 'dry_run',
945
+ manual_override: false,
946
+ })
947
+ if (typeof probeRun.workflow_run_log_id !== 'string' || probeRun.workflow_run_log_id.length === 0) {
948
+ throw new Error('dry-run probe returned no workflow_run_log_id')
949
+ }
950
+ ```
951
+
952
+ If the probe fails in production, escalate with the run response as
953
+ evidence — don't flip the workflow's status or rewrite its configs to
954
+ chase the error.
955
+
921
956
  # Branches
922
957
 
923
958
  - **Suppressed and unreachable are `:filtered`, not `:failed`.** Both