@alvera-ai/platform-sdk 0.14.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/action_status_updaters.md +113 -14
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +1 -1
- package/.agent/cookbook/contact-us-triage-with-llm.md +1 -1
- package/.agent/cookbook/dunning-sms-for-delinquent.md +1 -1
- package/.agent/cookbook/kyc-notification-on-account-activation.md +1 -1
- package/.agent/cookbook/paginated-restapi-poller.md +62 -8
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +1 -1
- package/.agent/cookbook/score-leads-with-llm-categorization.md +1 -1
- package/.agent/cookbook/triage-prospects-by-priority.md +1 -1
- package/.agent/cookbook/welcome-sms-for-customers.md +1 -1
- package/.agent/datalakes.md +44 -3
- package/package.json +1 -1
|
@@ -117,6 +117,10 @@ scope — do not loop.
|
|
|
117
117
|
{"external_id": "{{ notification.messageId }}", "status": "delivered"}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
> **Shape only — do not copy this mapping.** A hardcoded `"delivered"`
|
|
121
|
+
> records every event as delivered, failures included. Real bodies
|
|
122
|
+
> branch on the provider's status; see the SNS and MMS sections below.
|
|
123
|
+
|
|
120
124
|
It must render a **flat** JSON object:
|
|
121
125
|
|
|
122
126
|
```
|
|
@@ -139,15 +143,107 @@ events for messages this updater doesn't own.
|
|
|
139
143
|
Custom Liquid filters (`json_escape`, `e164`, `to_json`, ...) are
|
|
140
144
|
available here, as everywhere.
|
|
141
145
|
|
|
142
|
-
###
|
|
146
|
+
### SMS-over-SNS delivery tracking — two `custom` bodies
|
|
147
|
+
|
|
148
|
+
The SNS SMS delivery receipt is **binary and single-shot**: one terminal
|
|
149
|
+
outcome, `status` of `SUCCESS` or `FAILURE`. Its event shape is
|
|
150
|
+
**disjoint from EUM's** — `status`, `notification.messageId`,
|
|
151
|
+
`notification.timestamp`, `delivery.phoneCarrier`,
|
|
152
|
+
`delivery.providerResponse` all sit at the root. Do not carry an EUM
|
|
153
|
+
template over to SNS or vice versa.
|
|
154
|
+
|
|
155
|
+
Two convenient consequences, both unlike MMS:
|
|
156
|
+
|
|
157
|
+
- `delivered_at` is `notification.timestamp` — a UTC
|
|
158
|
+
`"YYYY-MM-DD HH:MM:SS.sss"` string Ecto casts directly, so **no date
|
|
159
|
+
filter is needed** (EUM needs `from_unix_ms` on its Unix-ms field).
|
|
160
|
+
- the DLR **does** surface the US carrier as `delivery.phoneCarrier`.
|
|
161
|
+
|
|
162
|
+
**`message_config`:**
|
|
163
|
+
|
|
164
|
+
```liquid
|
|
165
|
+
{"external_id": "{{ notification.messageId }}", "status": "{% if status == 'SUCCESS' %}delivered{% else %}failed{% endif %}", "status_description": "{{ delivery.providerResponse | json_escape }}"{% if status == 'SUCCESS' and notification.timestamp %}, "delivered_at": "{{ notification.timestamp }}"{% endif %}{% unless status == 'SUCCESS' %}, "failure_reason": "{{ status | json_escape }}"{% endunless %}{% if delivery.phoneCarrier and delivery.phoneCarrier != '' %}, "sms_carrier": "{{ delivery.phoneCarrier | json_escape }}"{% endif %}}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**`action_log_config`:**
|
|
169
|
+
|
|
170
|
+
```liquid
|
|
171
|
+
{"external_id": "{{ notification.messageId }}", "status": "{% if status == 'SUCCESS' %}delivered{% else %}failed{% endif %}", "status_description": "{{ delivery.providerResponse | json_escape }}"}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`status_description` is `delivery.providerResponse` verbatim on both
|
|
175
|
+
sides — the carrier's own text, on success and failure alike. Because
|
|
176
|
+
SNS's status is binary, `failure_reason` can only ever be `"FAILURE"`;
|
|
177
|
+
the granular carrier text stays in `status_description`.
|
|
178
|
+
|
|
179
|
+
**Note how the two SNS bodies differ from each other**: same status
|
|
180
|
+
mapping, fewer emitted fields on the action log (no `delivered_at`,
|
|
181
|
+
`sms_carrier`, or `failure_reason` — those are message concerns). That
|
|
182
|
+
is a *different* reason from MMS's, where the two also disagree on the
|
|
183
|
+
mapping itself. Both channels need two bodies; they need them for
|
|
184
|
+
different reasons.
|
|
185
|
+
|
|
186
|
+
### MMS delivery tracking needs TWO different templates — author both as `custom`
|
|
187
|
+
|
|
188
|
+
MMS delivery tracking is a `cloud_watch` updater. Author **both**
|
|
189
|
+
`message_config` and `action_log_config` as `custom` bodies — the two
|
|
190
|
+
render *different* JSON from the same event, so one body cannot serve
|
|
191
|
+
both. This is the single most-missed thing about updaters.
|
|
192
|
+
|
|
193
|
+
**Why they differ.** A message's status vocabulary includes
|
|
194
|
+
`customer_rejected`; an action log's does not. A recipient or carrier
|
|
195
|
+
rejection (`SPAM` / `BLOCKED` / `CARRIER_BLOCKED`) is a message-level
|
|
196
|
+
outcome — the action *did* deliver — so the action-log render must
|
|
197
|
+
**NO-OP** on those events while the message render records
|
|
198
|
+
`customer_rejected`. The message side also carries `delivered_at`,
|
|
199
|
+
`failure_reason`, and `sms_carrier`; only `status` +
|
|
200
|
+
`status_description` are ever applied to an action log.
|
|
201
|
+
|
|
202
|
+
AWS End User Messaging (`sms-voice`) event fields: top-level
|
|
203
|
+
`messageId`, `eventType` (`MEDIA_*`), `messageStatus`,
|
|
204
|
+
`messageStatusDescription`, `eventTimestamp` (Unix ms), `isFinal`.
|
|
205
|
+
|
|
206
|
+
**`message_config`** — map every status, emit the extra fields:
|
|
207
|
+
|
|
208
|
+
```liquid
|
|
209
|
+
{% case messageStatus %}
|
|
210
|
+
{% when 'DELIVERED' %}{% assign mapped_status = 'delivered' %}
|
|
211
|
+
{% when 'SUCCESSFUL' %}{% assign mapped_status = 'sent' %}
|
|
212
|
+
{% when 'PENDING' %}{% assign mapped_status = 'sent' %}
|
|
213
|
+
{% when 'QUEUED' %}{% assign mapped_status = 'queued' %}
|
|
214
|
+
{% when 'SPAM' %}{% assign mapped_status = 'customer_rejected' %}
|
|
215
|
+
{% when 'BLOCKED' %}{% assign mapped_status = 'customer_rejected' %}
|
|
216
|
+
{% when 'CARRIER_BLOCKED' %}{% assign mapped_status = 'customer_rejected' %}
|
|
217
|
+
{% else %}{% assign mapped_status = 'failed' %}
|
|
218
|
+
{% endcase %}
|
|
219
|
+
{"external_id": "{{ messageId }}", "status": "{{ mapped_status }}", "status_description": "{{ messageStatusDescription | json_escape }}"{% if messageStatus == 'DELIVERED' and eventTimestamp %}, "delivered_at": "{{ eventTimestamp | from_unix_ms }}"{% endif %}{% if mapped_status == 'failed' or mapped_status == 'customer_rejected' %}, "failure_reason": "{{ messageStatus | json_escape }}"{% endif %}{% if carrierName and carrierName != '' %}, "sms_carrier": "{{ carrierName | json_escape }}"{% endif %}}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**`action_log_config`** — the rejection statuses fall through to a
|
|
223
|
+
bare `external_id`, which applies nothing:
|
|
224
|
+
|
|
225
|
+
```liquid
|
|
226
|
+
{% assign al_status = '' %}{% case messageStatus %}{% when 'DELIVERED' %}{% assign al_status = 'delivered' %}{% when 'SUCCESSFUL' %}{% assign al_status = 'sent' %}{% when 'PENDING' %}{% assign al_status = 'sent' %}{% when 'QUEUED' %}{% assign al_status = 'queued' %}{% when 'SPAM', 'BLOCKED', 'CARRIER_BLOCKED' %}{% else %}{% assign al_status = 'failed' %}{% endcase %}{"external_id": "{{ messageId }}"{% if al_status != '' %}, "status": "{{ al_status }}", "status_description": "{{ messageStatusDescription | json_escape }}"{% endif %}}
|
|
227
|
+
```
|
|
143
228
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
229
|
+
Note the `{% when 'SPAM', 'BLOCKED', 'CARRIER_BLOCKED' %}` arm is
|
|
230
|
+
deliberately **empty** — it leaves `al_status` blank, so the render
|
|
231
|
+
emits `external_id` alone and the action log is untouched.
|
|
232
|
+
|
|
233
|
+
**Capture every event, not just the terminal one.** 2–3 events arrive
|
|
234
|
+
per message — interim `MEDIA_QUEUED` / `MEDIA_PENDING` /
|
|
235
|
+
`MEDIA_SUCCESSFUL` (`isFinal: false`), then one terminal event
|
|
236
|
+
(`isFinal: true`), up to 72h late and out of order. Apply is
|
|
237
|
+
idempotent and a monotonic guard only ever advances `status`, so a
|
|
238
|
+
late or interim lower-rank event can never demote a message that
|
|
239
|
+
already reached a higher state. Poll a window that overlaps
|
|
240
|
+
deliberately; nothing is dropped.
|
|
241
|
+
|
|
242
|
+
`queued_at` and `sent_at` are **your outbound pipeline's** times, set
|
|
243
|
+
when the platform enqueues and dispatches — a delivery poll never
|
|
244
|
+
overwrites them. Only `delivered_at` comes from the carrier event
|
|
245
|
+
(`DELIVERED`'s own `eventTimestamp` via `from_unix_ms`), so
|
|
246
|
+
re-polling a wide window does not drift it.
|
|
151
247
|
|
|
152
248
|
### Carrier information is available for SMS, not MMS
|
|
153
249
|
|
|
@@ -160,12 +256,15 @@ events — not something the platform chooses to drop:
|
|
|
160
256
|
under `delivery.phoneCarrier` (e.g. `"T-mobile USA Inc."`, with
|
|
161
257
|
numeric `mcc`/`mnc`). The SNS status template maps it to
|
|
162
258
|
`sms_carrier`.
|
|
163
|
-
- **MMS via AWS End User Messaging (`sms-voice`)** — the
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
259
|
+
- **MMS via AWS End User Messaging (`sms-voice`)** — the event *can*
|
|
260
|
+
carry `carrierName`, but it is DLR-populated and comes back
|
|
261
|
+
**absent for US destinations** (the US DLR does not surface it),
|
|
262
|
+
unlike the SNS path's `phoneCarrier`, whose US DLR does. So map it
|
|
263
|
+
when present — `{% if carrierName and carrierName != '' %}` — and
|
|
264
|
+
expect `sms_carrier` to stay null for US long-code traffic.
|
|
265
|
+
Verified against real AWS delivery events: the terminal
|
|
266
|
+
`MEDIA_DELIVERED` event for a US destination exposes
|
|
267
|
+
`totalCarrierFee` (a cost) and no `carrierName`.
|
|
169
268
|
|
|
170
269
|
AWS's Nov-2024 EventBridge-integration announcement states MMS
|
|
171
270
|
delivery events carry carrier information in EventBridge, but the
|
|
@@ -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:
|
|
@@ -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' },
|
|
@@ -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:
|
|
@@ -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 }}"}',
|
|
@@ -128,7 +128,17 @@ try {
|
|
|
128
128
|
updater_tool_id: ctx.restToolId,
|
|
129
129
|
sender_tool_ids: [ctx.senderToolId],
|
|
130
130
|
datalake_id: ctx.datalakeId,
|
|
131
|
-
|
|
131
|
+
// FLOOR the platform enforces: an array whose items are objects listing
|
|
132
|
+
// `external_id` in `required`. A bare `{ type: 'array' }` is a 422 at
|
|
133
|
+
// create — every event has to name the message it reconciles.
|
|
134
|
+
events_output_schema: {
|
|
135
|
+
type: 'array',
|
|
136
|
+
items: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
required: ['external_id'],
|
|
139
|
+
properties: { external_id: { type: 'string' } },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
132
142
|
pagination_context_output_schema: {
|
|
133
143
|
type: 'object',
|
|
134
144
|
required: ['has_next'],
|
|
@@ -140,7 +150,18 @@ try {
|
|
|
140
150
|
// Static on both — the cursor is captured below and never read back.
|
|
141
151
|
path: { type: 'custom', body: '/wiremock.domain/events' },
|
|
142
152
|
params: { type: 'custom', body: '{"event": "delivered"}' },
|
|
143
|
-
|
|
153
|
+
// Providers name their own id — `message-id` here, `messageId` / `sid`
|
|
154
|
+
// elsewhere. The events_template is where that becomes `external_id`:
|
|
155
|
+
// the apply path POPS that key off every rendered event to find the row
|
|
156
|
+
// it updates, so the render PROJECTS each event rather than passing the
|
|
157
|
+
// provider body through untouched.
|
|
158
|
+
events_template: {
|
|
159
|
+
type: 'custom',
|
|
160
|
+
body:
|
|
161
|
+
'[{% for item in response.items %}' +
|
|
162
|
+
'{"external_id": "{{ item.message.headers[\'message-id\'] }}", "event": "{{ item.event }}"}' +
|
|
163
|
+
'{% unless forloop.last %},{% endunless %}{% endfor %}]',
|
|
164
|
+
},
|
|
144
165
|
pagination_context_template: {
|
|
145
166
|
type: 'custom',
|
|
146
167
|
body:
|
|
@@ -150,11 +171,11 @@ try {
|
|
|
150
171
|
},
|
|
151
172
|
message_config: {
|
|
152
173
|
type: 'custom',
|
|
153
|
-
body: '{"external_id": "{{
|
|
174
|
+
body: '{"external_id": "{{ external_id }}", "status": "delivered"}',
|
|
154
175
|
},
|
|
155
176
|
action_log_config: {
|
|
156
177
|
type: 'custom',
|
|
157
|
-
body: '{"external_id": "{{
|
|
178
|
+
body: '{"external_id": "{{ external_id }}", "status": "delivered"}',
|
|
158
179
|
},
|
|
159
180
|
})
|
|
160
181
|
} catch (err) {
|
|
@@ -182,7 +203,14 @@ const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalake
|
|
|
182
203
|
updater_tool_id: ctx.restToolId,
|
|
183
204
|
sender_tool_ids: [ctx.senderToolId],
|
|
184
205
|
datalake_id: ctx.datalakeId,
|
|
185
|
-
events_output_schema: {
|
|
206
|
+
events_output_schema: {
|
|
207
|
+
type: 'array',
|
|
208
|
+
items: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
required: ['external_id'],
|
|
211
|
+
properties: { external_id: { type: 'string' } },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
186
214
|
pagination_context_output_schema: {
|
|
187
215
|
type: 'object',
|
|
188
216
|
required: ['has_next'],
|
|
@@ -201,7 +229,16 @@ const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalake
|
|
|
201
229
|
type: 'custom',
|
|
202
230
|
body: '{% unless msg.pagination_context %}{"event": "delivered"}{% endunless %}',
|
|
203
231
|
},
|
|
204
|
-
|
|
232
|
+
// The provider's own id (`message-id`) becomes `external_id` HERE — the
|
|
233
|
+
// apply path pops that key to find the row it reconciles, so project each
|
|
234
|
+
// event instead of passing the provider body through untouched.
|
|
235
|
+
events_template: {
|
|
236
|
+
type: 'custom',
|
|
237
|
+
body:
|
|
238
|
+
'[{% for item in response.items %}' +
|
|
239
|
+
'{"external_id": "{{ item.message.headers[\'message-id\'] }}", "event": "{{ item.event }}"}' +
|
|
240
|
+
'{% unless forloop.last %},{% endunless %}{% endfor %}]',
|
|
241
|
+
},
|
|
205
242
|
pagination_context_template: {
|
|
206
243
|
type: 'custom',
|
|
207
244
|
body:
|
|
@@ -211,11 +248,11 @@ const { data: asu } = await api.actionStatusUpdaters.create(tenantSlug, datalake
|
|
|
211
248
|
},
|
|
212
249
|
message_config: {
|
|
213
250
|
type: 'custom',
|
|
214
|
-
body: '{"external_id": "{{
|
|
251
|
+
body: '{"external_id": "{{ external_id }}", "status": "delivered"}',
|
|
215
252
|
},
|
|
216
253
|
action_log_config: {
|
|
217
254
|
type: 'custom',
|
|
218
|
-
body: '{"external_id": "{{
|
|
255
|
+
body: '{"external_id": "{{ external_id }}", "status": "delivered"}',
|
|
219
256
|
},
|
|
220
257
|
})
|
|
221
258
|
actionStatusUpdaterId = asu.id!
|
|
@@ -298,6 +335,23 @@ config and wait for another tick.
|
|
|
298
335
|
`events_output_schema` on every cycle; the pagination render is validated
|
|
299
336
|
against `pagination_context_output_schema` (which must require `has_next`).
|
|
300
337
|
Both schemas are REQUIRED for `restapi` updaters — blank is a 422.
|
|
338
|
+
- **Each schema has a FLOOR the platform enforces, above which the contract is
|
|
339
|
+
yours.** `events_output_schema` must describe an **array whose `items` are
|
|
340
|
+
objects listing `external_id` in `required`**; a bare `{ type: 'array' }` is a
|
|
341
|
+
422 at create. `pagination_context_output_schema` must be an **object listing
|
|
342
|
+
`has_next` in `required`** — that key is what ends the page loop. Demand more
|
|
343
|
+
of your provider on top if you like; only the floor is checked.
|
|
344
|
+
- **The floor exists because `external_id` is how reconciliation finds the row.**
|
|
345
|
+
`apply_status_update` pops that key off every rendered event, so mapping the
|
|
346
|
+
provider's own id (`message-id` / `messageId` / `sid`) into `external_id` is
|
|
347
|
+
the **`events_template`'s job** — project each event, never pass the provider
|
|
348
|
+
body through untouched. A schema that satisfies the floor while the template
|
|
349
|
+
emits raw provider rows creates cleanly and then reconciles nothing on every
|
|
350
|
+
cycle. `message_config` / `action_log_config` then read the **mapped**
|
|
351
|
+
`{{ external_id }}`, not the provider's original key.
|
|
352
|
+
- **`action_log_config` is required alongside `message_config`** for every
|
|
353
|
+
updater type, and both are cast against the *same* pinned reconciliation
|
|
354
|
+
schema — so one template body satisfies both.
|
|
301
355
|
- **A newly created poller is `status: 'active'`.** `cycle_detected` is only
|
|
302
356
|
ever set at runtime by the poll driver — never by a caller; you cannot
|
|
303
357
|
create your way into it.
|
|
@@ -286,7 +286,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
286
286
|
tool_id: toolId,
|
|
287
287
|
position: 0,
|
|
288
288
|
trigger_template: 'now',
|
|
289
|
-
idempotency_template: `{{ compliance_screening.id }}-${verdict}
|
|
289
|
+
idempotency_template: `{{ compliance_screening.id }}-${verdict}`,
|
|
290
290
|
tool_call: {
|
|
291
291
|
tool_call_type: 'sms_request',
|
|
292
292
|
to: { type: 'custom', body: '+15550000000' },
|
|
@@ -337,7 +337,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
337
337
|
tool_id: toolId,
|
|
338
338
|
position: 0,
|
|
339
339
|
trigger_template: 'now',
|
|
340
|
-
idempotency_template: `{{ subject_id }}-{{ action_id }}-${band}
|
|
340
|
+
idempotency_template: `{{ subject_id }}-{{ action_id }}-${band}`,
|
|
341
341
|
tool_call: {
|
|
342
342
|
tool_call_type: 'sms_request',
|
|
343
343
|
to: { type: 'custom', body: '+15551234567' },
|
|
@@ -309,7 +309,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
309
309
|
tool_id: toolId,
|
|
310
310
|
position: 0,
|
|
311
311
|
trigger_template: 'now',
|
|
312
|
-
idempotency_template: `{{ customer_id }}-${band}
|
|
312
|
+
idempotency_template: `{{ customer_id }}-${band}`,
|
|
313
313
|
tool_call: {
|
|
314
314
|
tool_call_type: 'sms_request',
|
|
315
315
|
to: { type: 'custom', body: '{{ mdm_output.regulated_customer.phone }}' },
|
|
@@ -182,7 +182,7 @@ const workflowResp = await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
|
182
182
|
decision_key: DECISION_KEY,
|
|
183
183
|
position: 0,
|
|
184
184
|
trigger_template: 'now',
|
|
185
|
-
idempotency_template: '{{ customer_id }}-{{ decision_key }}
|
|
185
|
+
idempotency_template: '{{ customer_id }}-{{ decision_key }}',
|
|
186
186
|
connected_app_id: connectedAppId,
|
|
187
187
|
connected_app_route: '/portal/welcome',
|
|
188
188
|
connected_app_metadata_template: '{"customer_id":"{{ mdm_output.customer.id }}"}',
|
package/.agent/datalakes.md
CHANGED
|
@@ -240,7 +240,7 @@ a hostname pattern; values like `"invalid_host!"` (with
|
|
|
240
240
|
non-hostname punctuation) are rejected.
|
|
241
241
|
|
|
242
242
|
This is the only field-format constraint surfaced before the
|
|
243
|
-
synchronous reachability probe runs (see §
|
|
243
|
+
synchronous reachability probe runs (see §8 Gotcha 9) — a
|
|
244
244
|
malformed identifier never gets as far as a connection attempt.
|
|
245
245
|
|
|
246
246
|
### Schema names must be unique within a database
|
|
@@ -576,7 +576,7 @@ agent-facing markdown, or the whole-domain catalog can be fetched
|
|
|
576
576
|
in one shot via `api.datasets.metadata(tenantSlug, datalakeSlug)`. Pair `.systemDatasets()` with a
|
|
577
577
|
`page_size: 1` probe per name as a post-migration smoke (proves
|
|
578
578
|
each industry-built table is queryable; the post-ready probe
|
|
579
|
-
code is in §
|
|
579
|
+
code is in §8 Gotcha 3).
|
|
580
580
|
|
|
581
581
|
```typescript
|
|
582
582
|
const { data: catalog } = await api.datalakes.systemDatasets(
|
|
@@ -641,7 +641,48 @@ const { data: csv } = await api.datalakes.executeSql(
|
|
|
641
641
|
) // csv is a string, not the JSON envelope
|
|
642
642
|
```
|
|
643
643
|
|
|
644
|
-
## 6.
|
|
644
|
+
## 6. Logical name → physical table
|
|
645
|
+
|
|
646
|
+
The name you author is not the name you query. Datasets are addressed
|
|
647
|
+
**logically** everywhere in a manifest or SDK call, and **physically** in SQL:
|
|
648
|
+
|
|
649
|
+
| You author | You query in SQL | Schema |
|
|
650
|
+
|---|---|---|
|
|
651
|
+
| `message` | `regulated_messages` | `app_regulated` |
|
|
652
|
+
| `action_log` | `action_logs` | `app_unregulated` |
|
|
653
|
+
| a generic table `clients` | `regulated_alvera_custom_clients` | `app_regulated` |
|
|
654
|
+
|
|
655
|
+
Custom (generic) tables carry an `alvera_custom_` prefix, and the regulated side
|
|
656
|
+
adds a further `regulated_` prefix on top. The schema follows `execute-sql`'s
|
|
657
|
+
`--mode` (`regulated` → `app_regulated`, `unregulated` → `app_unregulated`).
|
|
658
|
+
|
|
659
|
+
**Don't derive these by hand — read them.** Guessing column and table names is
|
|
660
|
+
how six turns get spent on `column "error_message" does not exist` (the real
|
|
661
|
+
column is `failure_reason`):
|
|
662
|
+
|
|
663
|
+
- `alvera get-metadata dataset --only <type>` prints the **physical dataset
|
|
664
|
+
name** plus every column, from the Ecto struct itself.
|
|
665
|
+
- `alvera get-metadata generic-table --only <slug>` prints the exact **primary
|
|
666
|
+
table alias** to use in a `WHERE` clause, with a worked `SELECT`.
|
|
667
|
+
|
|
668
|
+
An `execute-sql` failure on an undefined column, relation, table, or schema now
|
|
669
|
+
names these verbs in its error, so the correction path is one command away.
|
|
670
|
+
|
|
671
|
+
## 7. The roster owns the dedupe uri — don't inject it per source
|
|
672
|
+
|
|
673
|
+
MDM matches on the full identification triple, so a `source_uri` injected
|
|
674
|
+
per data source participates in matching. Two sources describing the same
|
|
675
|
+
subject then produce two different triples, and the same subject is written
|
|
676
|
+
**twice** instead of being reconciled onto one entity.
|
|
677
|
+
|
|
678
|
+
Derive the dedupe uri **once, at the roster**, and let every data source inherit
|
|
679
|
+
it. Never compute a per-source `source_uri` and expect MDM to see through it.
|
|
680
|
+
|
|
681
|
+
This divergence is **not visible through `get-metadata`**: the roster value and
|
|
682
|
+
the per-source values each look well-formed in isolation, and only the duplicate
|
|
683
|
+
rows downstream reveal the mismatch. Check the derivation, not the metadata.
|
|
684
|
+
|
|
685
|
+
## 8. Gotchas
|
|
645
686
|
|
|
646
687
|
1. **`create()` does NOT auto-enqueue migration.** The create response
|
|
647
688
|
returns immediately with `status: 'new'`; Datalake-DB-resident
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alvera-ai/platform-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Typed SDK for the Alvera platform API — manage data sources, tools, generic tables, AI agents, and action status updaters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|