@alvera-ai/platform-sdk 0.15.0 → 0.16.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/ai_agents.md +1 -0
- package/.agent/cookbook/_setup/foundation.md +62 -0
- package/.agent/cookbook/_setup/healthcare.md +62 -0
- package/.agent/cookbook/_setup/payments.md +62 -0
- package/.agent/cookbook/_setup/subscription.md +62 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +16 -8
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +11 -5
- package/.agent/cookbook/contact-us-triage-with-llm.md +11 -5
- package/.agent/cookbook/dunning-sms-for-delinquent.md +16 -8
- package/.agent/cookbook/kyc-notification-on-account-activation.md +16 -8
- package/.agent/cookbook/marketing-campaign-send.md +13 -7
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +11 -5
- package/.agent/cookbook/score-leads-with-llm-categorization.md +11 -5
- package/.agent/cookbook/triage-prospects-by-priority.md +11 -5
- package/.agent/cookbook/welcome-sms-for-customers.md +16 -8
- package/.agent/errors.md +7 -1
- package/.agent/interoperability_contracts.md +58 -0
- package/.agent/tools.md +98 -4
- package/.agent/workflows.md +137 -38
- package/dist/index.d.mts +966 -153
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +133 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/.agent/ai_agents.md
CHANGED
|
@@ -256,6 +256,63 @@ if (datalakeStatus !== 'ready') {
|
|
|
256
256
|
}
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
## 007 — a reusable "wait until the run has fired" helper
|
|
260
|
+
|
|
261
|
+
`workflows.run` only **schedules** a run. It records it and returns
|
|
262
|
+
immediately with `workflow_run_id` / `status` / `scheduled_at`; the
|
|
263
|
+
`workflow_run_log_id` and `batch_id` a scenario needs are written
|
|
264
|
+
later, when the run actually fires, and are read back from
|
|
265
|
+
`workflowRuns.get`.
|
|
266
|
+
|
|
267
|
+
Two traps live in that gap, so every foundation scenario shares one
|
|
268
|
+
helper rather than re-deriving it:
|
|
269
|
+
|
|
270
|
+
- **Poll until `workflow_run_log_id` is a string — NOT until `status`
|
|
271
|
+
leaves `'scheduled'`.** Those are different moments: the run reaches
|
|
272
|
+
`processing` first and writes the log id a beat later. A predicate on
|
|
273
|
+
status alone releases you to read a `null`, and because
|
|
274
|
+
`typeof null === 'object'` the symptom is a baffling *"expected
|
|
275
|
+
string, got object"* rather than an obvious nil.
|
|
276
|
+
- **Raise on `failed` carrying `failure_reason` rather than polling to
|
|
277
|
+
the deadline.** A scenario blocked on a run that will never fire
|
|
278
|
+
should say why on the first read, not thirty seconds later behind a
|
|
279
|
+
generic timeout.
|
|
280
|
+
|
|
281
|
+
The helper is stored on the shared `ctx` bag rather than declared as a
|
|
282
|
+
plain function because each numbered step compiles into its own `it()`
|
|
283
|
+
block — a bare `function` here would not be in scope for the steps that
|
|
284
|
+
call it.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
const FIRED_TIMEOUT_MS = 60_000
|
|
288
|
+
const FIRED_POLL_MS = 1_000
|
|
289
|
+
|
|
290
|
+
ctx.waitForFiredRun = async (
|
|
291
|
+
runDatalakeSlug: string,
|
|
292
|
+
runId: string,
|
|
293
|
+
timeoutMs: number = FIRED_TIMEOUT_MS,
|
|
294
|
+
): Promise<{ workflowRunLogId: string; batchId: string | null }> => {
|
|
295
|
+
const deadline = Date.now() + timeoutMs
|
|
296
|
+
let lastStatus: string | undefined
|
|
297
|
+
while (Date.now() < deadline) {
|
|
298
|
+
const { data } = await api.workflowRuns.get(tenantSlug, runDatalakeSlug, runId)
|
|
299
|
+
lastStatus = data.status
|
|
300
|
+
if (data.status === 'failed') {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`workflow run ${runId} failed: ${data.failure_reason ?? 'no failure_reason given'}`,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
if (typeof data.workflow_run_log_id === 'string') {
|
|
306
|
+
return { workflowRunLogId: data.workflow_run_log_id, batchId: data.batch_id ?? null }
|
|
307
|
+
}
|
|
308
|
+
await new Promise((r) => setTimeout(r, FIRED_POLL_MS))
|
|
309
|
+
}
|
|
310
|
+
throw new Error(
|
|
311
|
+
`workflow run ${runId} never fired within ${timeoutMs}ms (last status: ${lastStatus})`,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
259
316
|
# Rollback
|
|
260
317
|
|
|
261
318
|
The cookbook doctest harness does not currently tear down the
|
|
@@ -282,6 +339,11 @@ After this setup runs, the closure-scoped slots are populated as:
|
|
|
282
339
|
- `ctx.tenantApiKey` — the tenant's publishable API key (minted in
|
|
283
340
|
setup via `admin.createTenantApiKey`); thread it into any
|
|
284
341
|
additional tenant-scoped `createSession` a scenario performs
|
|
342
|
+
- `ctx.waitForFiredRun(datalakeSlug, runId)` — polls `workflowRuns.get`
|
|
343
|
+
until a scheduled run has actually fired, then returns its
|
|
344
|
+
`{ workflowRunLogId, batchId }`. Use it after every `workflows.run`;
|
|
345
|
+
reading `workflow_run_log_id` off the run response returns `null`
|
|
346
|
+
because run-workflow only schedules (see §007)
|
|
285
347
|
|
|
286
348
|
Scenario cookbooks under `industry: foundation` start at their own
|
|
287
349
|
`§001` with these slots already populated.
|
|
@@ -257,6 +257,63 @@ if (datalakeStatus !== 'ready') {
|
|
|
257
257
|
}
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
+
## 007 — a reusable "wait until the run has fired" helper
|
|
261
|
+
|
|
262
|
+
`workflows.run` only **schedules** a run. It records it and returns
|
|
263
|
+
immediately with `workflow_run_id` / `status` / `scheduled_at`; the
|
|
264
|
+
`workflow_run_log_id` and `batch_id` a scenario needs are written
|
|
265
|
+
later, when the run actually fires, and are read back from
|
|
266
|
+
`workflowRuns.get`.
|
|
267
|
+
|
|
268
|
+
Two traps live in that gap, so every healthcare scenario shares one
|
|
269
|
+
helper rather than re-deriving it:
|
|
270
|
+
|
|
271
|
+
- **Poll until `workflow_run_log_id` is a string — NOT until `status`
|
|
272
|
+
leaves `'scheduled'`.** Those are different moments: the run reaches
|
|
273
|
+
`processing` first and writes the log id a beat later. A predicate on
|
|
274
|
+
status alone releases you to read a `null`, and because
|
|
275
|
+
`typeof null === 'object'` the symptom is a baffling *"expected
|
|
276
|
+
string, got object"* rather than an obvious nil.
|
|
277
|
+
- **Raise on `failed` carrying `failure_reason` rather than polling to
|
|
278
|
+
the deadline.** A scenario blocked on a run that will never fire
|
|
279
|
+
should say why on the first read, not thirty seconds later behind a
|
|
280
|
+
generic timeout.
|
|
281
|
+
|
|
282
|
+
The helper is stored on the shared `ctx` bag rather than declared as a
|
|
283
|
+
plain function because each numbered step compiles into its own `it()`
|
|
284
|
+
block — a bare `function` here would not be in scope for the steps that
|
|
285
|
+
call it.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const FIRED_TIMEOUT_MS = 60_000
|
|
289
|
+
const FIRED_POLL_MS = 1_000
|
|
290
|
+
|
|
291
|
+
ctx.waitForFiredRun = async (
|
|
292
|
+
runDatalakeSlug: string,
|
|
293
|
+
runId: string,
|
|
294
|
+
timeoutMs: number = FIRED_TIMEOUT_MS,
|
|
295
|
+
): Promise<{ workflowRunLogId: string; batchId: string | null }> => {
|
|
296
|
+
const deadline = Date.now() + timeoutMs
|
|
297
|
+
let lastStatus: string | undefined
|
|
298
|
+
while (Date.now() < deadline) {
|
|
299
|
+
const { data } = await api.workflowRuns.get(tenantSlug, runDatalakeSlug, runId)
|
|
300
|
+
lastStatus = data.status
|
|
301
|
+
if (data.status === 'failed') {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`workflow run ${runId} failed: ${data.failure_reason ?? 'no failure_reason given'}`,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
if (typeof data.workflow_run_log_id === 'string') {
|
|
307
|
+
return { workflowRunLogId: data.workflow_run_log_id, batchId: data.batch_id ?? null }
|
|
308
|
+
}
|
|
309
|
+
await new Promise((r) => setTimeout(r, FIRED_POLL_MS))
|
|
310
|
+
}
|
|
311
|
+
throw new Error(
|
|
312
|
+
`workflow run ${runId} never fired within ${timeoutMs}ms (last status: ${lastStatus})`,
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
260
317
|
# Rollback
|
|
261
318
|
|
|
262
319
|
The cookbook doctest harness does not currently tear down the
|
|
@@ -283,6 +340,11 @@ After this setup runs, the closure-scoped slots are populated as:
|
|
|
283
340
|
- `ctx.tenantApiKey` — the tenant's publishable API key (minted in
|
|
284
341
|
setup via `admin.createTenantApiKey`); thread it into any
|
|
285
342
|
additional tenant-scoped `createSession` a scenario performs
|
|
343
|
+
- `ctx.waitForFiredRun(datalakeSlug, runId)` — polls `workflowRuns.get`
|
|
344
|
+
until a scheduled run has actually fired, then returns its
|
|
345
|
+
`{ workflowRunLogId, batchId }`. Use it after every `workflows.run`;
|
|
346
|
+
reading `workflow_run_log_id` off the run response returns `null`
|
|
347
|
+
because run-workflow only schedules (see §007)
|
|
286
348
|
|
|
287
349
|
Scenario cookbooks under `industry: healthcare` start at their own
|
|
288
350
|
`§001` with these slots already populated.
|
|
@@ -261,6 +261,63 @@ if (datalakeStatus !== 'ready') {
|
|
|
261
261
|
}
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
+
## 007 — a reusable "wait until the run has fired" helper
|
|
265
|
+
|
|
266
|
+
`workflows.run` only **schedules** a run. It records it and returns
|
|
267
|
+
immediately with `workflow_run_id` / `status` / `scheduled_at`; the
|
|
268
|
+
`workflow_run_log_id` and `batch_id` a scenario needs are written
|
|
269
|
+
later, when the run actually fires, and are read back from
|
|
270
|
+
`workflowRuns.get`.
|
|
271
|
+
|
|
272
|
+
Two traps live in that gap, so every payments scenario shares one
|
|
273
|
+
helper rather than re-deriving it:
|
|
274
|
+
|
|
275
|
+
- **Poll until `workflow_run_log_id` is a string — NOT until `status`
|
|
276
|
+
leaves `'scheduled'`.** Those are different moments: the run reaches
|
|
277
|
+
`processing` first and writes the log id a beat later. A predicate on
|
|
278
|
+
status alone releases you to read a `null`, and because
|
|
279
|
+
`typeof null === 'object'` the symptom is a baffling *"expected
|
|
280
|
+
string, got object"* rather than an obvious nil.
|
|
281
|
+
- **Raise on `failed` carrying `failure_reason` rather than polling to
|
|
282
|
+
the deadline.** A scenario blocked on a run that will never fire
|
|
283
|
+
should say why on the first read, not thirty seconds later behind a
|
|
284
|
+
generic timeout.
|
|
285
|
+
|
|
286
|
+
The helper is stored on the shared `ctx` bag rather than declared as a
|
|
287
|
+
plain function because each numbered step compiles into its own `it()`
|
|
288
|
+
block — a bare `function` here would not be in scope for the steps that
|
|
289
|
+
call it.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const FIRED_TIMEOUT_MS = 60_000
|
|
293
|
+
const FIRED_POLL_MS = 1_000
|
|
294
|
+
|
|
295
|
+
ctx.waitForFiredRun = async (
|
|
296
|
+
runDatalakeSlug: string,
|
|
297
|
+
runId: string,
|
|
298
|
+
timeoutMs: number = FIRED_TIMEOUT_MS,
|
|
299
|
+
): Promise<{ workflowRunLogId: string; batchId: string | null }> => {
|
|
300
|
+
const deadline = Date.now() + timeoutMs
|
|
301
|
+
let lastStatus: string | undefined
|
|
302
|
+
while (Date.now() < deadline) {
|
|
303
|
+
const { data } = await api.workflowRuns.get(tenantSlug, runDatalakeSlug, runId)
|
|
304
|
+
lastStatus = data.status
|
|
305
|
+
if (data.status === 'failed') {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`workflow run ${runId} failed: ${data.failure_reason ?? 'no failure_reason given'}`,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
if (typeof data.workflow_run_log_id === 'string') {
|
|
311
|
+
return { workflowRunLogId: data.workflow_run_log_id, batchId: data.batch_id ?? null }
|
|
312
|
+
}
|
|
313
|
+
await new Promise((r) => setTimeout(r, FIRED_POLL_MS))
|
|
314
|
+
}
|
|
315
|
+
throw new Error(
|
|
316
|
+
`workflow run ${runId} never fired within ${timeoutMs}ms (last status: ${lastStatus})`,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
264
321
|
# Rollback
|
|
265
322
|
|
|
266
323
|
The cookbook doctest harness does not currently tear down the
|
|
@@ -287,6 +344,11 @@ After this setup runs, the closure-scoped slots are populated as:
|
|
|
287
344
|
- `ctx.tenantApiKey` — the tenant's publishable API key (minted in
|
|
288
345
|
setup via `admin.createTenantApiKey`); thread it into any
|
|
289
346
|
additional tenant-scoped `createSession` a scenario performs
|
|
347
|
+
- `ctx.waitForFiredRun(datalakeSlug, runId)` — polls `workflowRuns.get`
|
|
348
|
+
until a scheduled run has actually fired, then returns its
|
|
349
|
+
`{ workflowRunLogId, batchId }`. Use it after every `workflows.run`;
|
|
350
|
+
reading `workflow_run_log_id` off the run response returns `null`
|
|
351
|
+
because run-workflow only schedules (see §007)
|
|
290
352
|
|
|
291
353
|
Scenario cookbooks under `industry: payments` start at their
|
|
292
354
|
own `§001` with these slots already populated.
|
|
@@ -260,6 +260,63 @@ if (datalakeStatus !== 'ready') {
|
|
|
260
260
|
}
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
+
## 007 — a reusable "wait until the run has fired" helper
|
|
264
|
+
|
|
265
|
+
`workflows.run` only **schedules** a run. It records it and returns
|
|
266
|
+
immediately with `workflow_run_id` / `status` / `scheduled_at`; the
|
|
267
|
+
`workflow_run_log_id` and `batch_id` a scenario needs are written
|
|
268
|
+
later, when the run actually fires, and are read back from
|
|
269
|
+
`workflowRuns.get`.
|
|
270
|
+
|
|
271
|
+
Two traps live in that gap, so every subscription scenario shares one
|
|
272
|
+
helper rather than re-deriving it:
|
|
273
|
+
|
|
274
|
+
- **Poll until `workflow_run_log_id` is a string — NOT until `status`
|
|
275
|
+
leaves `'scheduled'`.** Those are different moments: the run reaches
|
|
276
|
+
`processing` first and writes the log id a beat later. A predicate on
|
|
277
|
+
status alone releases you to read a `null`, and because
|
|
278
|
+
`typeof null === 'object'` the symptom is a baffling *"expected
|
|
279
|
+
string, got object"* rather than an obvious nil.
|
|
280
|
+
- **Raise on `failed` carrying `failure_reason` rather than polling to
|
|
281
|
+
the deadline.** A scenario blocked on a run that will never fire
|
|
282
|
+
should say why on the first read, not thirty seconds later behind a
|
|
283
|
+
generic timeout.
|
|
284
|
+
|
|
285
|
+
The helper is stored on the shared `ctx` bag rather than declared as a
|
|
286
|
+
plain function because each numbered step compiles into its own `it()`
|
|
287
|
+
block — a bare `function` here would not be in scope for the steps that
|
|
288
|
+
call it.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const FIRED_TIMEOUT_MS = 60_000
|
|
292
|
+
const FIRED_POLL_MS = 1_000
|
|
293
|
+
|
|
294
|
+
ctx.waitForFiredRun = async (
|
|
295
|
+
runDatalakeSlug: string,
|
|
296
|
+
runId: string,
|
|
297
|
+
timeoutMs: number = FIRED_TIMEOUT_MS,
|
|
298
|
+
): Promise<{ workflowRunLogId: string; batchId: string | null }> => {
|
|
299
|
+
const deadline = Date.now() + timeoutMs
|
|
300
|
+
let lastStatus: string | undefined
|
|
301
|
+
while (Date.now() < deadline) {
|
|
302
|
+
const { data } = await api.workflowRuns.get(tenantSlug, runDatalakeSlug, runId)
|
|
303
|
+
lastStatus = data.status
|
|
304
|
+
if (data.status === 'failed') {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`workflow run ${runId} failed: ${data.failure_reason ?? 'no failure_reason given'}`,
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
if (typeof data.workflow_run_log_id === 'string') {
|
|
310
|
+
return { workflowRunLogId: data.workflow_run_log_id, batchId: data.batch_id ?? null }
|
|
311
|
+
}
|
|
312
|
+
await new Promise((r) => setTimeout(r, FIRED_POLL_MS))
|
|
313
|
+
}
|
|
314
|
+
throw new Error(
|
|
315
|
+
`workflow run ${runId} never fired within ${timeoutMs}ms (last status: ${lastStatus})`,
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
263
320
|
# Rollback
|
|
264
321
|
|
|
265
322
|
The cookbook doctest harness does not currently tear down the
|
|
@@ -286,6 +343,11 @@ After this setup runs, the closure-scoped slots are populated as:
|
|
|
286
343
|
- `ctx.tenantApiKey` — the tenant's publishable API key (minted in
|
|
287
344
|
setup via `admin.createTenantApiKey`); thread it into any
|
|
288
345
|
additional tenant-scoped `createSession` a scenario performs
|
|
346
|
+
- `ctx.waitForFiredRun(datalakeSlug, runId)` — polls `workflowRuns.get`
|
|
347
|
+
until a scheduled run has actually fired, then returns its
|
|
348
|
+
`{ workflowRunLogId, batchId }`. Use it after every `workflows.run`;
|
|
349
|
+
reading `workflow_run_log_id` off the run response returns `null`
|
|
350
|
+
because run-workflow only schedules (see §007)
|
|
289
351
|
|
|
290
352
|
Scenario cookbooks under `industry: subscription` start at
|
|
291
353
|
their own `§001` with these slots already populated.
|
|
@@ -159,6 +159,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
159
159
|
description: 'Sends a review-request SMS with a connected-app form link after a fulfilled appointment.',
|
|
160
160
|
dataset_type: 'appointment',
|
|
161
161
|
status: 'live',
|
|
162
|
+
tags: ['appointments', 'review'],
|
|
162
163
|
skip_mdm_resolution: false,
|
|
163
164
|
filter_config: {
|
|
164
165
|
type: 'custom',
|
|
@@ -476,9 +477,11 @@ batches §009 ingested (`ra` is the regulated-appointments alias
|
|
|
476
477
|
the run-query exposes).
|
|
477
478
|
|
|
478
479
|
The SMS action's `trigger_template: 'now'` dispatches the action
|
|
479
|
-
immediately rather than
|
|
480
|
-
terminal status on its own — poll `batchLogs.refresh`
|
|
481
|
-
leaves `:pending`.
|
|
480
|
+
immediately rather than deferring it, so once the run fires it
|
|
481
|
+
reaches a terminal status on its own — poll `batchLogs.refresh`
|
|
482
|
+
until it leaves `:pending`. The run itself is still scheduled:
|
|
483
|
+
`workflows.run` records it and returns, which is why the setup
|
|
484
|
+
file's `ctx.waitForFiredRun` sits between the call and the log id. A `:partial` status is expected and fine here:
|
|
482
485
|
one row passed and one was filtered.
|
|
483
486
|
|
|
484
487
|
```typescript
|
|
@@ -487,8 +490,11 @@ const runResp = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSl
|
|
|
487
490
|
mode: 'live',
|
|
488
491
|
manual_override: false,
|
|
489
492
|
})
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
// run-workflow only SCHEDULES the run. The log id and batch id are
|
|
494
|
+
// written when it fires, so read them back via workflowRuns.get.
|
|
495
|
+
const fired = await ctx.waitForFiredRun(datalakeSlug, runResp.data.workflow_run_id)
|
|
496
|
+
ctx.runLogId = fired.workflowRunLogId
|
|
497
|
+
ctx.runBatchId = fired.batchId!
|
|
492
498
|
|
|
493
499
|
const deadline = Date.now() + 120_000
|
|
494
500
|
let status: string | null = null
|
|
@@ -692,14 +698,16 @@ if (wfRow.status !== 'live') {
|
|
|
692
698
|
}
|
|
693
699
|
// Behavioural probe — a dry run against a selection no row can match:
|
|
694
700
|
// the pipeline executes end-to-end, the final action call is
|
|
695
|
-
// intercepted, and the acknowledgement carries the run
|
|
701
|
+
// intercepted, and the acknowledgement carries the scheduled run id.
|
|
696
702
|
const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
|
|
697
703
|
sql_where_clause: "ra.batch_id = 'test-never-matching-batch'",
|
|
698
704
|
mode: 'dry_run',
|
|
699
705
|
manual_override: false,
|
|
700
706
|
})
|
|
701
|
-
|
|
702
|
-
|
|
707
|
+
// The run-log id does not exist until the run fires — wait, do not read a null.
|
|
708
|
+
const probeFired = await ctx.waitForFiredRun(datalakeSlug, probeRun.workflow_run_id)
|
|
709
|
+
if (probeFired.workflowRunLogId.length === 0) {
|
|
710
|
+
throw new Error('dry-run probe never produced a workflow_run_log_id')
|
|
703
711
|
}
|
|
704
712
|
```
|
|
705
713
|
|
|
@@ -157,6 +157,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
157
157
|
description: "Sends a happy-birthday SMS on each contact's next birthday — pure-Liquid trigger does the year-roll math.",
|
|
158
158
|
dataset_type: 'legal_entity',
|
|
159
159
|
status: 'live',
|
|
160
|
+
tags: ['lifecycle', 'birthday'],
|
|
160
161
|
skip_mdm_resolution: false,
|
|
161
162
|
filter_config: {
|
|
162
163
|
type: 'custom',
|
|
@@ -398,8 +399,11 @@ const runResp = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSl
|
|
|
398
399
|
mode: 'live',
|
|
399
400
|
manual_override: false,
|
|
400
401
|
})
|
|
401
|
-
|
|
402
|
-
|
|
402
|
+
// run-workflow only SCHEDULES the run. The log id and batch id are
|
|
403
|
+
// written when it fires, so read them back via workflowRuns.get.
|
|
404
|
+
const fired = await ctx.waitForFiredRun(datalakeSlug, runResp.data.workflow_run_id)
|
|
405
|
+
ctx.runLogId = fired.workflowRunLogId
|
|
406
|
+
ctx.runBatchId = fired.batchId!
|
|
403
407
|
|
|
404
408
|
const deadline = Date.now() + 60_000
|
|
405
409
|
let totalWels = 0
|
|
@@ -594,7 +598,7 @@ if (wfRow.status !== 'live') {
|
|
|
594
598
|
}
|
|
595
599
|
// Behavioural probe — a dry run against a selection no row can match:
|
|
596
600
|
// the pipeline executes end-to-end, the final action call is
|
|
597
|
-
// intercepted, and the acknowledgement carries the run
|
|
601
|
+
// intercepted, and the acknowledgement carries the scheduled run id. The
|
|
598
602
|
// clause must speak this workflow's selection dialect — the dataset
|
|
599
603
|
// alias is `rle` here, the same alias the live run above uses.
|
|
600
604
|
const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
|
|
@@ -602,8 +606,10 @@ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx
|
|
|
602
606
|
mode: 'dry_run',
|
|
603
607
|
manual_override: false,
|
|
604
608
|
})
|
|
605
|
-
|
|
606
|
-
|
|
609
|
+
// The run-log id does not exist until the run fires — wait, do not read a null.
|
|
610
|
+
const probeFired = await ctx.waitForFiredRun(datalakeSlug, probeRun.workflow_run_id)
|
|
611
|
+
if (probeFired.workflowRunLogId.length === 0) {
|
|
612
|
+
throw new Error('dry-run probe never produced a workflow_run_log_id')
|
|
607
613
|
}
|
|
608
614
|
```
|
|
609
615
|
|
|
@@ -310,6 +310,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
310
310
|
generic_table_id: genericTableId,
|
|
311
311
|
skip_mdm_resolution: true,
|
|
312
312
|
status: 'live',
|
|
313
|
+
tags: ['support', 'triage'],
|
|
313
314
|
filter_config: {
|
|
314
315
|
type: 'custom',
|
|
315
316
|
body: 'true',
|
|
@@ -478,8 +479,11 @@ const runResp = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSl
|
|
|
478
479
|
mode: 'live',
|
|
479
480
|
manual_override: true,
|
|
480
481
|
})
|
|
481
|
-
|
|
482
|
-
|
|
482
|
+
// run-workflow only SCHEDULES the run. The log id and batch id are
|
|
483
|
+
// written when it fires, so read them back via workflowRuns.get.
|
|
484
|
+
const fired = await ctx.waitForFiredRun(datalakeSlug, runResp.data.workflow_run_id)
|
|
485
|
+
ctx.runLogId = fired.workflowRunLogId
|
|
486
|
+
ctx.runBatchId = fired.batchId!
|
|
483
487
|
|
|
484
488
|
const deadline = Date.now() + 240_000
|
|
485
489
|
let status: string | null = null
|
|
@@ -560,7 +564,7 @@ if (wfRow.status !== 'live') {
|
|
|
560
564
|
}
|
|
561
565
|
// Behavioural probe — a dry run against a selection no row can match:
|
|
562
566
|
// the pipeline executes end-to-end, the final action call is
|
|
563
|
-
// intercepted, and the acknowledgement carries the run
|
|
567
|
+
// intercepted, and the acknowledgement carries the scheduled run id. The
|
|
564
568
|
// clause speaks this workflow's selection dialect: a GENERIC-TABLE
|
|
565
569
|
// dataset is addressed by its own columns (no `ra.` dataset alias —
|
|
566
570
|
// that alias exists only for system-dataset selections).
|
|
@@ -569,8 +573,10 @@ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx
|
|
|
569
573
|
mode: 'dry_run',
|
|
570
574
|
manual_override: false,
|
|
571
575
|
})
|
|
572
|
-
|
|
573
|
-
|
|
576
|
+
// The run-log id does not exist until the run fires — wait, do not read a null.
|
|
577
|
+
const probeFired = await ctx.waitForFiredRun(datalakeSlug, probeRun.workflow_run_id)
|
|
578
|
+
if (probeFired.workflowRunLogId.length === 0) {
|
|
579
|
+
throw new Error('dry-run probe never produced a workflow_run_log_id')
|
|
574
580
|
}
|
|
575
581
|
```
|
|
576
582
|
|
|
@@ -162,6 +162,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
162
162
|
description: 'Sends a payment-reminder SMS to delinquent contracted customers with a self-serve pay link.',
|
|
163
163
|
dataset_type: 'customer',
|
|
164
164
|
status: 'live',
|
|
165
|
+
tags: ['billing', 'dunning'],
|
|
165
166
|
filter_config: {
|
|
166
167
|
type: 'custom',
|
|
167
168
|
body: FILTER_BODY,
|
|
@@ -395,9 +396,11 @@ exactly the two batches §008 ingested (`ra` is the
|
|
|
395
396
|
regulated-customer alias the run-query exposes).
|
|
396
397
|
|
|
397
398
|
The SMS action's `trigger_template: 'now'` dispatches the action
|
|
398
|
-
immediately rather than
|
|
399
|
-
terminal status on its own — poll `batchLogs.refresh`
|
|
400
|
-
leaves `:pending`.
|
|
399
|
+
immediately rather than deferring it, so once the run fires it
|
|
400
|
+
reaches a terminal status on its own — poll `batchLogs.refresh`
|
|
401
|
+
until it leaves `:pending`. The run itself is still scheduled:
|
|
402
|
+
`workflows.run` records it and returns, which is why the setup
|
|
403
|
+
file's `ctx.waitForFiredRun` sits between the call and the log id. A `:partial` status is expected and fine here:
|
|
401
404
|
one row passed and one was filtered.
|
|
402
405
|
|
|
403
406
|
```typescript
|
|
@@ -406,8 +409,11 @@ const runResp = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSl
|
|
|
406
409
|
mode: 'live',
|
|
407
410
|
manual_override: false,
|
|
408
411
|
})
|
|
409
|
-
|
|
410
|
-
|
|
412
|
+
// run-workflow only SCHEDULES the run. The log id and batch id are
|
|
413
|
+
// written when it fires, so read them back via workflowRuns.get.
|
|
414
|
+
const fired = await ctx.waitForFiredRun(datalakeSlug, runResp.data.workflow_run_id)
|
|
415
|
+
ctx.runLogId = fired.workflowRunLogId
|
|
416
|
+
ctx.runBatchId = fired.batchId!
|
|
411
417
|
|
|
412
418
|
const deadline = Date.now() + 120_000
|
|
413
419
|
let status: string | null = null
|
|
@@ -550,14 +556,16 @@ if (wfRow.status !== 'live') {
|
|
|
550
556
|
}
|
|
551
557
|
// Behavioural probe — a dry run against a selection no row can match:
|
|
552
558
|
// the pipeline executes end-to-end, the final action call is
|
|
553
|
-
// intercepted, and the acknowledgement carries the run
|
|
559
|
+
// intercepted, and the acknowledgement carries the scheduled run id.
|
|
554
560
|
const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
|
|
555
561
|
sql_where_clause: "ra.batch_id = 'test-never-matching-batch'",
|
|
556
562
|
mode: 'dry_run',
|
|
557
563
|
manual_override: false,
|
|
558
564
|
})
|
|
559
|
-
|
|
560
|
-
|
|
565
|
+
// The run-log id does not exist until the run fires — wait, do not read a null.
|
|
566
|
+
const probeFired = await ctx.waitForFiredRun(datalakeSlug, probeRun.workflow_run_id)
|
|
567
|
+
if (probeFired.workflowRunLogId.length === 0) {
|
|
568
|
+
throw new Error('dry-run probe never produced a workflow_run_log_id')
|
|
561
569
|
}
|
|
562
570
|
```
|
|
563
571
|
|
|
@@ -160,6 +160,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
160
160
|
description: 'Sends a KYC-notification SMS for newly activated payment accounts with a self-serve KYC portal link.',
|
|
161
161
|
dataset_type: 'payment_account',
|
|
162
162
|
status: 'live',
|
|
163
|
+
tags: ['compliance', 'kyc'],
|
|
163
164
|
filter_config: {
|
|
164
165
|
type: 'custom',
|
|
165
166
|
body: FILTER_BODY,
|
|
@@ -393,9 +394,11 @@ batches §008 ingested (`rpa` is the regulated-payment-account
|
|
|
393
394
|
alias the run-query exposes).
|
|
394
395
|
|
|
395
396
|
The SMS action's `trigger_template: 'now'` dispatches the action
|
|
396
|
-
immediately rather than
|
|
397
|
-
terminal status on its own — poll `batchLogs.refresh`
|
|
398
|
-
leaves `:pending`.
|
|
397
|
+
immediately rather than deferring it, so once the run fires it
|
|
398
|
+
reaches a terminal status on its own — poll `batchLogs.refresh`
|
|
399
|
+
until it leaves `:pending`. The run itself is still scheduled:
|
|
400
|
+
`workflows.run` records it and returns, which is why the setup
|
|
401
|
+
file's `ctx.waitForFiredRun` sits between the call and the log id. A `:partial` status is expected and fine here:
|
|
399
402
|
one row passed and one was filtered.
|
|
400
403
|
|
|
401
404
|
```typescript
|
|
@@ -404,8 +407,11 @@ const runResp = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSl
|
|
|
404
407
|
mode: 'live',
|
|
405
408
|
manual_override: false,
|
|
406
409
|
})
|
|
407
|
-
|
|
408
|
-
|
|
410
|
+
// run-workflow only SCHEDULES the run. The log id and batch id are
|
|
411
|
+
// written when it fires, so read them back via workflowRuns.get.
|
|
412
|
+
const fired = await ctx.waitForFiredRun(datalakeSlug, runResp.data.workflow_run_id)
|
|
413
|
+
ctx.runLogId = fired.workflowRunLogId
|
|
414
|
+
ctx.runBatchId = fired.batchId!
|
|
409
415
|
|
|
410
416
|
const deadline = Date.now() + 120_000
|
|
411
417
|
let status: string | null = null
|
|
@@ -548,7 +554,7 @@ if (wfRow.status !== 'live') {
|
|
|
548
554
|
}
|
|
549
555
|
// Behavioural probe — a dry run against a selection no row can match:
|
|
550
556
|
// the pipeline executes end-to-end, the final action call is
|
|
551
|
-
// intercepted, and the acknowledgement carries the run
|
|
557
|
+
// intercepted, and the acknowledgement carries the scheduled run id. The
|
|
552
558
|
// clause must speak this workflow's selection dialect — the dataset
|
|
553
559
|
// alias is `rpa` here, the same alias the live run above uses.
|
|
554
560
|
const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.workflowSlug, {
|
|
@@ -556,8 +562,10 @@ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx
|
|
|
556
562
|
mode: 'dry_run',
|
|
557
563
|
manual_override: false,
|
|
558
564
|
})
|
|
559
|
-
|
|
560
|
-
|
|
565
|
+
// The run-log id does not exist until the run fires — wait, do not read a null.
|
|
566
|
+
const probeFired = await ctx.waitForFiredRun(datalakeSlug, probeRun.workflow_run_id)
|
|
567
|
+
if (probeFired.workflowRunLogId.length === 0) {
|
|
568
|
+
throw new Error('dry-run probe never produced a workflow_run_log_id')
|
|
561
569
|
}
|
|
562
570
|
```
|
|
563
571
|
|
|
@@ -273,7 +273,7 @@ const ROSTER_MDM = `{% assign p = msg %}
|
|
|
273
273
|
"legal_entity_type": "business",
|
|
274
274
|
"business_name": "{{ p.business_name | json_escape }}",
|
|
275
275
|
"identifiers": [
|
|
276
|
-
{"system": "{{ p.source_uri | json_escape }}", "value": "{{ p.direct_customer_id | json_escape }}"}
|
|
276
|
+
{"system": "{{ p.source_uri | json_escape }}", "value": "{{ p.direct_customer_id | json_escape }}", "type": "digital_identifier"}
|
|
277
277
|
]
|
|
278
278
|
}`
|
|
279
279
|
|
|
@@ -376,7 +376,7 @@ const AUDIENCE_MDM = `{% assign p = msg %}
|
|
|
376
376
|
{% if first_name != "" %}"first_name": "{{ first_name | json_escape }}",{% endif %}
|
|
377
377
|
{% if last_name != "" %}"last_name": "{{ last_name | json_escape }}",{% endif %}
|
|
378
378
|
"identifiers": [
|
|
379
|
-
{"system": "{{ p.source_uri | json_escape }}", "value": "{{ handle | json_escape }}"}
|
|
379
|
+
{"system": "{{ p.source_uri | json_escape }}", "value": "{{ handle | json_escape }}", "type": "digital_identifier"}
|
|
380
380
|
]
|
|
381
381
|
}`
|
|
382
382
|
|
|
@@ -609,6 +609,7 @@ const { data: workflow } = await api.workflows.create(tenantSlug, datalakeSlug,
|
|
|
609
609
|
dataset_type: 'generic_table',
|
|
610
610
|
generic_table_id: ctx.audienceTableId,
|
|
611
611
|
status: 'live',
|
|
612
|
+
tags: ['marketing', 'loyalty'],
|
|
612
613
|
skip_mdm_resolution: false,
|
|
613
614
|
filter_config: { type: 'custom', body: CAMPAIGN_FILTER, output_schema: { type: 'boolean' } },
|
|
614
615
|
decision_config: {
|
|
@@ -690,7 +691,10 @@ const { data: run } = await api.workflows.run(tenantSlug, datalakeSlug, ctx.work
|
|
|
690
691
|
mode: 'live',
|
|
691
692
|
manual_override: false,
|
|
692
693
|
})
|
|
693
|
-
|
|
694
|
+
// run-workflow only SCHEDULES the run. The log id is written when it
|
|
695
|
+
// fires, so read it back via workflowRuns.get.
|
|
696
|
+
const fired = await ctx.waitForFiredRun(datalakeSlug, run.workflow_run_id)
|
|
697
|
+
ctx.runLogId = fired.workflowRunLogId
|
|
694
698
|
|
|
695
699
|
const deadline = Date.now() + 180_000
|
|
696
700
|
let byStatus: Record<string, number> = {}
|
|
@@ -844,7 +848,7 @@ const REPLY_MDM = `{% assign p = msg %}
|
|
|
844
848
|
{
|
|
845
849
|
"legal_entity_type": "individual",
|
|
846
850
|
"identifiers": [
|
|
847
|
-
{"system": "{{ p.source_uri | json_escape }}", "value": "{{ p.handle | json_escape }}"}
|
|
851
|
+
{"system": "{{ p.source_uri | json_escape }}", "value": "{{ p.handle | json_escape }}", "type": "digital_identifier"}
|
|
848
852
|
]
|
|
849
853
|
}`
|
|
850
854
|
|
|
@@ -935,7 +939,7 @@ if (wfRow.status !== 'live') {
|
|
|
935
939
|
}
|
|
936
940
|
// Behavioural probe — a dry run against a selection no row can match:
|
|
937
941
|
// the pipeline executes end-to-end, the final action call is
|
|
938
|
-
// intercepted, and the acknowledgement carries the run
|
|
942
|
+
// intercepted, and the acknowledgement carries the scheduled run id. The
|
|
939
943
|
// clause speaks this workflow's selection dialect: a GENERIC-TABLE
|
|
940
944
|
// audience is addressed by its own columns (no `ra.` dataset alias —
|
|
941
945
|
// that alias exists only for system-dataset selections).
|
|
@@ -944,8 +948,10 @@ const { data: probeRun } = await api.workflows.run(tenantSlug, datalakeSlug, ctx
|
|
|
944
948
|
mode: 'dry_run',
|
|
945
949
|
manual_override: false,
|
|
946
950
|
})
|
|
947
|
-
|
|
948
|
-
|
|
951
|
+
// The run-log id does not exist until the run fires — wait, do not read a null.
|
|
952
|
+
const probeFired = await ctx.waitForFiredRun(datalakeSlug, probeRun.workflow_run_id)
|
|
953
|
+
if (probeFired.workflowRunLogId.length === 0) {
|
|
954
|
+
throw new Error('dry-run probe never produced a workflow_run_log_id')
|
|
949
955
|
}
|
|
950
956
|
```
|
|
951
957
|
|