@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.
- package/.agent/AGENTS.md +82 -144
- package/.agent/account_management.md +2 -2
- package/.agent/action_logs.md +4 -4
- package/.agent/ai_agents.md +28 -21
- package/.agent/ai_sandbox.md +49 -39
- package/.agent/connected_apps.md +3 -3
- package/.agent/cookbook/_fixtures/README.md +1 -1
- package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_generic_table.liquid +1 -1
- package/.agent/cookbook/_fixtures/organic-marketing/_lead_submissions_foundation_legal_entity.liquid +80 -0
- package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_mdm.liquid +2 -1
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_generic_table.liquid +57 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_legal_entity.liquid +30 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_mdm.liquid +44 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_generic_table.liquid +57 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_legal_entity.liquid +36 -0
- package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_mdm.liquid +41 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_generic_table.liquid +70 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_legal_entity.liquid +52 -0
- package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_mdm.liquid +42 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_generic_table.liquid +38 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_legal_entity.liquid +48 -0
- package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_mdm.liquid +49 -0
- package/.agent/cookbook/organic-marketing.md +2801 -0
- package/.agent/cookbook/payments-compliance.md +2180 -0
- package/.agent/cookbook/primary-care.md +2175 -0
- package/.agent/cookbook/subscription-saas.md +2403 -0
- package/.agent/data_activation_clients.md +65 -52
- package/.agent/datalakes.md +338 -171
- package/.agent/errors.md +3 -3
- package/.agent/generic_tables.md +151 -62
- package/.agent/interoperability_contracts.md +57 -22
- package/.agent/mdm.md +136 -153
- package/.agent/messages.md +36 -34
- package/.agent/mock-services.md +1 -1
- package/.agent/mutations.md +2 -2
- package/.agent/templates.md +14 -13
- package/.agent/tools.md +63 -21
- package/.agent/type_naming.md +13 -13
- package/.agent/workflows.md +99 -53
- package/README.md +2 -2
- package/dist/bin/platform-sdk.mjs +33 -47
- package/dist/bin/platform-sdk.mjs.map +1 -1
- package/dist/index.d.mts +565 -379
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +494 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +0 -88
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +0 -47
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +0 -24
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +0 -38
- package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_compliance_screening.liquid +0 -59
- package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_mdm.liquid +0 -36
- package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_mdm.liquid +0 -30
- package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_payment_account.liquid +0 -55
- package/.agent/cookbook/_fixtures/subscription/_customers_subscription_mdm.liquid +0 -20
- package/.agent/cookbook/_setup/foundation.md +0 -359
- package/.agent/cookbook/_setup/healthcare.md +0 -361
- package/.agent/cookbook/_setup/payments.md +0 -365
- package/.agent/cookbook/_setup/subscription.md +0 -364
- package/.agent/cookbook/action-status-updaters.md +0 -278
- package/.agent/cookbook/ai-agent-invoke.md +0 -279
- package/.agent/cookbook/appointment-review-sms-workflow.md +0 -801
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -696
- package/.agent/cookbook/bulk-ingest.md +0 -302
- package/.agent/cookbook/contact-us-triage-with-llm.md +0 -663
- package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -659
- package/.agent/cookbook/generic-tables.md +0 -244
- package/.agent/cookbook/invite-team.md +0 -200
- package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -661
- package/.agent/cookbook/marketing-campaign-send.md +0 -1044
- package/.agent/cookbook/paginated-restapi-poller.md +0 -383
- package/.agent/cookbook/rest-fetch.md +0 -273
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -773
- package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -665
- package/.agent/cookbook/system-templates.md +0 -165
- package/.agent/cookbook/talk-to-data.md +0 -178
- package/.agent/cookbook/triage-prospects-by-priority.md +0 -571
- package/.agent/cookbook/welcome-sms-for-customers.md +0 -647
- /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/memorandum-of-association-01.png +0 -0
- /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/sample_two_page.pdf +0 -0
- /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/_customers_subscription_customer.liquid +0 -0
- /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/stripe_customers_batch1.csv +0 -0
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Foundation industry bootstrap setup
|
|
3
|
-
summary: Auth as root, sign up + confirm a fresh industry-admin user, create a foundation tenant + datalake. Shared by every foundation scenario cookbook.
|
|
4
|
-
industry: foundation
|
|
5
|
-
slug: foundation
|
|
6
|
-
vitest_source:
|
|
7
|
-
- integration-tests/tests/foundation/bootstrap.test.ts
|
|
8
|
-
status: green
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Problem
|
|
12
|
-
|
|
13
|
-
Every foundation scenario cookbook needs the same starting point:
|
|
14
|
-
a fresh tenant on the platform with a foundation-domain datalake
|
|
15
|
-
attached, accessed by an industry-admin user. Authoring this
|
|
16
|
-
prelude inline inside every scenario cookbook would duplicate the
|
|
17
|
-
auth + tenant + datalake chain across two cookbooks (and four more
|
|
18
|
-
across the other industries). The validator auto-discovers this
|
|
19
|
-
file from each cookbook's `industry: foundation` front-matter and
|
|
20
|
-
inlines its numbered steps ahead of the scenario's own, so the
|
|
21
|
-
generated bun:test spec is hermetic without the cookbook author
|
|
22
|
-
having to copy-paste this prelude.
|
|
23
|
-
|
|
24
|
-
# Composition
|
|
25
|
-
|
|
26
|
-
| Resource provisioned | Owner |
|
|
27
|
-
|-------------------------------|-------------|
|
|
28
|
-
| Root session | setup |
|
|
29
|
-
| Industry-admin user (`sarah`) | setup |
|
|
30
|
-
| Foundation tenant | setup |
|
|
31
|
-
| Foundation datalake | setup |
|
|
32
|
-
|
|
33
|
-
# Walkthrough
|
|
34
|
-
|
|
35
|
-
## 001 — root admin signs in and provisions a fresh industry-admin user
|
|
36
|
-
|
|
37
|
-
Authenticate as the platform's root admin (`admin@dev.local` /
|
|
38
|
-
`devpassword` in local dev) via the tenantless bootstrap login —
|
|
39
|
-
keyless by structural necessity (no tenant exists yet to scope a
|
|
40
|
-
key to), and a dev/test-only surface. Then sign up a per-run industry-admin
|
|
41
|
-
user (`sarah`) under a unique email derived from `runSuffix`, and
|
|
42
|
-
confirm them so they can sign in. Both the signup and the confirm
|
|
43
|
-
endpoints require root scope.
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
ctx.rootSession = await createBootstrapSession({
|
|
47
|
-
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
48
|
-
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
49
|
-
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
50
|
-
})
|
|
51
|
-
ctx.rootApi = createIsolatedPlatformApi({
|
|
52
|
-
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
53
|
-
sessionToken: ctx.rootSession.sessionToken,
|
|
54
|
-
apiKey: '',
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
|
|
58
|
-
ctx.sarahPassword = 'CookbookPass1!'
|
|
59
|
-
|
|
60
|
-
const signUpResp = await ctx.rootApi.admin.signUp({
|
|
61
|
-
email: ctx.sarahEmail,
|
|
62
|
-
password: ctx.sarahPassword,
|
|
63
|
-
first_name: 'Cookbook',
|
|
64
|
-
last_name: 'Sarah',
|
|
65
|
-
})
|
|
66
|
-
ctx.sarahUserId = signUpResp.data.id
|
|
67
|
-
await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## 002 — sarah signs in (tenantless) and creates the foundation tenant
|
|
71
|
-
|
|
72
|
-
Sarah signs in for the first time without a tenant scope (no
|
|
73
|
-
tenant exists yet for her), then immediately creates a fresh
|
|
74
|
-
foundation tenant. The tenant's server-derived `slug` is captured
|
|
75
|
-
into the closure-scoped `tenantSlug` slot for downstream steps.
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
ctx.sarahTenantlessSession = await createBootstrapSession({
|
|
79
|
-
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
80
|
-
email: ctx.sarahEmail,
|
|
81
|
-
password: ctx.sarahPassword,
|
|
82
|
-
})
|
|
83
|
-
ctx.sarahTenantlessApi = createIsolatedPlatformApi({
|
|
84
|
-
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
85
|
-
sessionToken: ctx.sarahTenantlessSession.sessionToken,
|
|
86
|
-
apiKey: '',
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
|
|
90
|
-
name: `Cookbook Foundation ${runSuffix}`,
|
|
91
|
-
})
|
|
92
|
-
tenantSlug = tenantResp.data.slug!
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## 003 — sarah signs in tenant-scoped — the canonical client
|
|
96
|
-
|
|
97
|
-
Re-mint sarah's session with the new tenant slug. This bearer is
|
|
98
|
-
the canonical tenant-scoped client every subsequent step uses; it
|
|
99
|
-
is assigned to the closure-scoped `api` slot so the scenario
|
|
100
|
-
cookbook's steps inherit it via inlining.
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// A tenant-scoped login requires X-API-Key — mint a public_api key for
|
|
104
|
-
// the fresh tenant via the platform-admin side door (root Bearer). In the
|
|
105
|
-
// web console this is Settings -> API Keys; any of the tenant's keys
|
|
106
|
-
// satisfies the login gate, and the minted Bearer inherits this key's
|
|
107
|
-
// origin policy.
|
|
108
|
-
const { data: mintedKey } = await ctx.rootApi.admin.createTenantApiKey(tenantSlug, {
|
|
109
|
-
name: `Cookbook Bootstrap Key ${runSuffix}`,
|
|
110
|
-
data_access_mode: 'unregulated',
|
|
111
|
-
})
|
|
112
|
-
ctx.tenantApiKey = mintedKey.api_key
|
|
113
|
-
|
|
114
|
-
const sarahTenantSession = await createSession({
|
|
115
|
-
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
116
|
-
email: ctx.sarahEmail,
|
|
117
|
-
password: ctx.sarahPassword,
|
|
118
|
-
tenantSlug,
|
|
119
|
-
apiKey: ctx.tenantApiKey,
|
|
120
|
-
})
|
|
121
|
-
api = createIsolatedPlatformApi({
|
|
122
|
-
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
123
|
-
sessionToken: sarahTenantSession.sessionToken,
|
|
124
|
-
apiKey: ctx.tenantApiKey,
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## 004 — sarah creates the foundation datalake
|
|
129
|
-
|
|
130
|
-
Provision a foundation-domain datalake on the new tenant. The DB
|
|
131
|
-
schemas are scoped to the cookbook run via `runSuffix` so parallel
|
|
132
|
-
cookbook runs do not collide on schema names. Local-dev defaults
|
|
133
|
-
(`postgres` on `localhost:5432`, `alvera_dev_foundation`) match
|
|
134
|
-
the seeded `dev.exs` setup; LocalStack S3 (`localhost:4566`)
|
|
135
|
-
serves the unregulated + regulated buckets. The datalake's
|
|
136
|
-
server-derived `slug` is captured into `datalakeSlug`.
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
const DB_HOST = 'localhost'
|
|
140
|
-
const DB_PORT = 5432
|
|
141
|
-
const DB_USER = 'postgres'
|
|
142
|
-
const DB_PASS = 'postgres'
|
|
143
|
-
const DB_NAME = 'alvera_dev_foundation'
|
|
144
|
-
const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
|
|
145
|
-
const REG_SCHEMA = `cookbook_${runSuffix}_reg`
|
|
146
|
-
|
|
147
|
-
const S3 = {
|
|
148
|
-
cloud_storage_type: 'aws' as const,
|
|
149
|
-
region: 'us-east-1',
|
|
150
|
-
access_key_id: 'test',
|
|
151
|
-
secret_access_key: 'test',
|
|
152
|
-
endpoint: 'http://localhost:4566',
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const datalakeResp = await api.datalakes.create(tenantSlug, {
|
|
156
|
-
name: `Cookbook Foundation Datalake ${runSuffix}`,
|
|
157
|
-
description: 'Foundation datalake provisioned by cookbook doctest.',
|
|
158
|
-
data_domain: 'foundation',
|
|
159
|
-
timezone: 'America/New_York',
|
|
160
|
-
pool_size: 5,
|
|
161
|
-
|
|
162
|
-
unregulated_db_writer_host: DB_HOST,
|
|
163
|
-
unregulated_db_writer_port: DB_PORT,
|
|
164
|
-
unregulated_db_writer_name: DB_NAME,
|
|
165
|
-
unregulated_db_writer_schema: UNREG_SCHEMA,
|
|
166
|
-
unregulated_db_writer_auth_method: 'password',
|
|
167
|
-
unregulated_db_writer_user: DB_USER,
|
|
168
|
-
unregulated_db_writer_pass: DB_PASS,
|
|
169
|
-
unregulated_db_writer_enable_ssl: false,
|
|
170
|
-
unregulated_db_reader_host: DB_HOST,
|
|
171
|
-
unregulated_db_reader_port: DB_PORT,
|
|
172
|
-
unregulated_db_reader_name: DB_NAME,
|
|
173
|
-
unregulated_db_reader_schema: UNREG_SCHEMA,
|
|
174
|
-
unregulated_db_reader_auth_method: 'password',
|
|
175
|
-
unregulated_db_reader_user: DB_USER,
|
|
176
|
-
unregulated_db_reader_pass: DB_PASS,
|
|
177
|
-
unregulated_db_reader_enable_ssl: false,
|
|
178
|
-
|
|
179
|
-
regulated_data_db_writer_host: DB_HOST,
|
|
180
|
-
regulated_data_db_writer_port: DB_PORT,
|
|
181
|
-
regulated_data_db_writer_name: DB_NAME,
|
|
182
|
-
regulated_data_db_writer_schema: REG_SCHEMA,
|
|
183
|
-
regulated_data_db_writer_auth_method: 'password',
|
|
184
|
-
regulated_data_db_writer_user: DB_USER,
|
|
185
|
-
regulated_data_db_writer_pass: DB_PASS,
|
|
186
|
-
regulated_data_db_writer_enable_ssl: false,
|
|
187
|
-
regulated_data_db_reader_host: DB_HOST,
|
|
188
|
-
regulated_data_db_reader_port: DB_PORT,
|
|
189
|
-
regulated_data_db_reader_name: DB_NAME,
|
|
190
|
-
regulated_data_db_reader_schema: REG_SCHEMA,
|
|
191
|
-
regulated_data_db_reader_auth_method: 'password',
|
|
192
|
-
regulated_data_db_reader_user: DB_USER,
|
|
193
|
-
regulated_data_db_reader_pass: DB_PASS,
|
|
194
|
-
regulated_data_db_reader_enable_ssl: false,
|
|
195
|
-
|
|
196
|
-
unregulated_cloud_storage: { ...S3, bucket: 'foundation-lake-unregulated' },
|
|
197
|
-
regulated_cloud_storage: { ...S3, bucket: 'foundation-lake-regulated' },
|
|
198
|
-
})
|
|
199
|
-
datalakeSlug = datalakeResp.data.slug!
|
|
200
|
-
// `tools.create` and a few other endpoints accept the datalake by
|
|
201
|
-
// UUID in the request body rather than only the slug in the URL,
|
|
202
|
-
// so keep it on `ctx` for downstream steps that need it.
|
|
203
|
-
ctx.datalakeId = datalakeResp.data.id!
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## 005 — enqueue the datalake migrations
|
|
207
|
-
|
|
208
|
-
`datalakes.create` persists the datalake row in `:new` status but
|
|
209
|
-
does not itself run the schema migrations. Migration is a
|
|
210
|
-
separately-triggered async job so the operator controls when the
|
|
211
|
-
(potentially slow) per-industry DDL runs. `datalakes.migrate`
|
|
212
|
-
enqueues the `DatalakeMigrationWorker` Oban job and returns
|
|
213
|
-
immediately with `status: 'enqueued'` plus the Oban `job_id`. The
|
|
214
|
-
poll in §006 then waits for that worker to finish — without this
|
|
215
|
-
call the datalake would sit at `:new` forever.
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
|
|
219
|
-
if (migrateResp.data.status !== 'enqueued') {
|
|
220
|
-
throw new Error(
|
|
221
|
-
`datalake migration not enqueued (status: ${migrateResp.data.status})`,
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## 006 — poll the datalake until status is ready
|
|
227
|
-
|
|
228
|
-
Datalake migrations are async — the §005 migrate call enqueued a
|
|
229
|
-
`DatalakeMigrationWorker` Oban job that runs the per-industry
|
|
230
|
-
schema migrations, deploys PostgREST roles, and provisions the
|
|
231
|
-
service-account bot user. Downstream resource creates (tools,
|
|
232
|
-
workflows) only need `datalakeId` to be persisted (which it is
|
|
233
|
-
the moment §004 returns), but any scenario step that touches
|
|
234
|
-
the regulated/unregulated DBs or the dataset infrastructure
|
|
235
|
-
requires `status: 'ready'`. Polling here makes every foundation
|
|
236
|
-
scenario cookbook deterministic regardless of how long the cold
|
|
237
|
-
migration takes on a given host; the loop exits the moment the
|
|
238
|
-
datalake reaches `:ready`, so the 5-minute cap is paid only in
|
|
239
|
-
failure mode.
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
const READY_TIMEOUT_MS = 5 * 60_000
|
|
243
|
-
const READY_POLL_MS = 5_000
|
|
244
|
-
const deadline = Date.now() + READY_TIMEOUT_MS
|
|
245
|
-
let datalakeStatus: string | undefined
|
|
246
|
-
while (Date.now() < deadline) {
|
|
247
|
-
const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
|
|
248
|
-
datalakeStatus = data.status
|
|
249
|
-
if (datalakeStatus === 'ready') break
|
|
250
|
-
await new Promise((r) => setTimeout(r, READY_POLL_MS))
|
|
251
|
-
}
|
|
252
|
-
if (datalakeStatus !== 'ready') {
|
|
253
|
-
throw new Error(
|
|
254
|
-
`datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
```
|
|
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
|
-
|
|
316
|
-
# Rollback
|
|
317
|
-
|
|
318
|
-
The cookbook doctest harness does not currently tear down the
|
|
319
|
-
created tenant / datalake / user — each run mints fresh names via
|
|
320
|
-
`runSuffix` so reruns do not collide, and the seeded local DB is
|
|
321
|
-
cheap to reset (`mix ecto.reset` on the platform). When the
|
|
322
|
-
validator graduates to continuous integration at Milestone 18
|
|
323
|
-
(running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
|
|
324
|
-
no real state is created at all.
|
|
325
|
-
|
|
326
|
-
# Outcome
|
|
327
|
-
|
|
328
|
-
After this setup runs, the closure-scoped slots are populated as:
|
|
329
|
-
|
|
330
|
-
- `api` — a tenant-scoped `PlatformApi` client authenticated as
|
|
331
|
-
the industry-admin user
|
|
332
|
-
- `tenantSlug` — server-derived slug of the fresh foundation
|
|
333
|
-
tenant
|
|
334
|
-
- `datalakeSlug` — server-derived slug of the fresh foundation
|
|
335
|
-
datalake
|
|
336
|
-
- `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
|
|
337
|
-
credentials (kept on `ctx` because no scenario cookbook needs
|
|
338
|
-
to re-authenticate by default)
|
|
339
|
-
- `ctx.tenantApiKey` — the tenant's publishable API key (minted in
|
|
340
|
-
setup via `admin.createTenantApiKey`); thread it into any
|
|
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)
|
|
347
|
-
|
|
348
|
-
Scenario cookbooks under `industry: foundation` start at their own
|
|
349
|
-
`§001` with these slots already populated.
|
|
350
|
-
|
|
351
|
-
# See also
|
|
352
|
-
|
|
353
|
-
- `.agent/datalakes.md` — datalake create body shape (regulated +
|
|
354
|
-
unregulated tier configuration)
|
|
355
|
-
- `.agent/AGENTS.md` § SDK auth + client construction — root vs
|
|
356
|
-
tenantless vs tenant-scoped session scopes
|
|
357
|
-
- `integration-tests/tests/foundation/bootstrap.test.ts` — the
|
|
358
|
-
green test these snippets are lifted from (the §7b sibling
|
|
359
|
-
datalake creation is out of scope for this lean cookbook setup)
|