@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.
- package/.agent/account_management.md +31 -0
- package/.agent/action_status_updaters.md +273 -25
- package/.agent/ai_sandbox.md +4 -2
- package/.agent/connected_apps.md +7 -2
- package/.agent/cookbook/action-status-updaters.md +73 -14
- package/.agent/cookbook/ai-agent-invoke.md +36 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +32 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +35 -1
- package/.agent/cookbook/bulk-ingest.md +48 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +36 -1
- package/.agent/cookbook/dunning-sms-for-delinquent.md +33 -1
- package/.agent/cookbook/generic-tables.md +40 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +35 -1
- package/.agent/cookbook/marketing-campaign-send.md +35 -0
- package/.agent/cookbook/paginated-restapi-poller.md +383 -0
- package/.agent/cookbook/rest-fetch.md +27 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +35 -1
- package/.agent/cookbook/score-leads-with-llm-categorization.md +36 -1
- package/.agent/cookbook/system-templates.md +36 -0
- package/.agent/cookbook/talk-to-data.md +39 -0
- package/.agent/cookbook/triage-prospects-by-priority.md +33 -1
- package/.agent/cookbook/welcome-sms-for-customers.md +33 -1
- package/.agent/datalakes.md +68 -3
- package/.agent/interoperability_contracts.md +29 -0
- package/.agent/tool-call-configs.md +11 -0
- package/.agent/tools.md +103 -36
- package/.agent/type_naming.md +4 -0
- package/dist/index.d.mts +189 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- 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 }}
|
|
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}
|
|
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 }}
|
|
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 }}
|
|
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
|