@alvera-ai/platform-sdk 0.10.0-rc.2 → 0.10.0-rc.21
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 +440 -0
- package/.agent/account_management.md +455 -0
- package/.agent/action_status_updaters.md +262 -0
- package/.agent/ai_agents.md +423 -0
- package/.agent/ai_sandbox.md +265 -0
- package/.agent/async.md +111 -0
- package/.agent/connected_apps.md +407 -0
- package/.agent/cookbook/_fixtures/README.md +99 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
- package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
- package/.agent/cookbook/_setup/foundation.md +277 -0
- package/.agent/cookbook/_setup/healthcare.md +279 -0
- package/.agent/cookbook/_setup/payment_risk.md +283 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +603 -0
- package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +711 -0
- package/.agent/cookbook/score-leads-with-llm-categorization.md +602 -0
- package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
- package/.agent/data_activation_clients.md +557 -0
- package/.agent/data_sources.md +234 -0
- package/.agent/datalakes.md +712 -0
- package/.agent/debugging.md +137 -0
- package/.agent/errors.md +196 -0
- package/.agent/generic_tables.md +351 -0
- package/.agent/interoperability_contracts.md +351 -0
- package/.agent/mdm.md +293 -0
- package/.agent/mutations.md +152 -0
- package/.agent/templates.md +98 -0
- package/.agent/tool-call-configs.md +90 -0
- package/.agent/tools.md +546 -0
- package/.agent/type_naming.md +131 -0
- package/.agent/workflows.md +601 -0
- package/README.md +46 -0
- package/dist/bin/platform-sdk.d.mts +1 -0
- package/dist/bin/platform-sdk.mjs +106 -0
- package/dist/bin/platform-sdk.mjs.map +1 -0
- package/dist/index.d.mts +1200 -43201
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1859 -7319
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -10
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
# Workflows
|
|
2
|
+
|
|
3
|
+
A **workflow** is the event-driven decision pipeline that fires on
|
|
4
|
+
top of ingested dataset rows. Every workflow has the same shape:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
dataset row event
|
|
8
|
+
│
|
|
9
|
+
▼
|
|
10
|
+
filter — does this row qualify? (Liquid expression → boolean)
|
|
11
|
+
│
|
|
12
|
+
▼
|
|
13
|
+
enrichment — optional AI step that derives extra context (see §3)
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
decision — which action key fires? (Liquid array of strings, OR
|
|
17
|
+
an AI agent's output)
|
|
18
|
+
│
|
|
19
|
+
▼
|
|
20
|
+
action(s) — scheduled tool calls keyed by decision_key
|
|
21
|
+
(SMS, email, voice, export, ...)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
SDK namespace: `api.workflows`. Sub-namespaces for run-time logs:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
api.workflows.batchLogs — one log row per workflow run (batch)
|
|
28
|
+
api.workflows.workflowLogs — one log row per dataset row processed
|
|
29
|
+
(workflow execution logs)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Workflows are **Datalake-DB-resident** — the parent datalake must
|
|
33
|
+
be `status: 'ready'` and every referenced resource (tools,
|
|
34
|
+
connected apps, AI agents) must exist before POST.
|
|
35
|
+
|
|
36
|
+
## 1. Wire shape
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import type {
|
|
40
|
+
WorkflowRequestWritable,
|
|
41
|
+
WorkflowResponse,
|
|
42
|
+
} from '@alvera-ai/platform-sdk'
|
|
43
|
+
|
|
44
|
+
const { data: created } = await api.workflows.create(
|
|
45
|
+
tenantSlug,
|
|
46
|
+
datalakeSlug,
|
|
47
|
+
{
|
|
48
|
+
name: 'Appointment Review SMS',
|
|
49
|
+
description: 'Send a review-request SMS after a fulfilled appointment',
|
|
50
|
+
dataset_type: 'appointment', // the dataset this workflow listens on
|
|
51
|
+
status: 'live', // 'live' | 'draft'
|
|
52
|
+
filter_config: {
|
|
53
|
+
type: 'custom',
|
|
54
|
+
body: `{% if appointment.source_uri == "12345.example.com" %}true{% endif %}`,
|
|
55
|
+
output_schema: '{"type":"boolean"}',
|
|
56
|
+
},
|
|
57
|
+
decision_config: {
|
|
58
|
+
type: 'custom',
|
|
59
|
+
body: `["send_appointment_review_sms"]`,
|
|
60
|
+
output_schema: '{"type":"array","items":{"type":"string"}}',
|
|
61
|
+
},
|
|
62
|
+
context_datasets: [
|
|
63
|
+
{
|
|
64
|
+
dataset_type: 'message',
|
|
65
|
+
where_clause: `rm.patient_id = '{{ patient_id }}' AND rm.sent_at > NOW() - INTERVAL '6 months'`,
|
|
66
|
+
limit: 1,
|
|
67
|
+
position: 0,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
actions: [
|
|
71
|
+
{
|
|
72
|
+
action_type: 'sms',
|
|
73
|
+
tool_id: smsToolId,
|
|
74
|
+
decision_key: 'send_appointment_review_sms',
|
|
75
|
+
position: 0,
|
|
76
|
+
trigger_template: 'now', // 'now' = next available slot
|
|
77
|
+
idempotency_template:
|
|
78
|
+
'{{ patient_id }}-{{ appointment.unregulated_appointment_id }}-{{ decision_key }}',
|
|
79
|
+
connected_app_id: connectedAppId,
|
|
80
|
+
connected_app_route: '/forms/review',
|
|
81
|
+
connected_app_metadata_template:
|
|
82
|
+
'{"appointment_id":"{{ appointment.unregulated_appointment_id }}"}',
|
|
83
|
+
tool_call: {
|
|
84
|
+
tool_call_type: 'sms_request',
|
|
85
|
+
to: { type: 'custom', body: '{{ patient_phone }}' },
|
|
86
|
+
body: { type: 'custom', body: 'Hi {{ patient_first_name }}, please share your feedback at {{ connected_app_form_url }}' },
|
|
87
|
+
sms_type: 'transactional',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`actions`, `context_datasets`, and (for agent-driven workflows)
|
|
96
|
+
`ai_agents` are all inline arrays — one POST builds the whole
|
|
97
|
+
workflow.
|
|
98
|
+
|
|
99
|
+
## 2. Rules the type cannot encode
|
|
100
|
+
|
|
101
|
+
### `filter_config` / `decision_config` are Liquid expressions with typed output
|
|
102
|
+
|
|
103
|
+
Both configs follow the same shape: `{ type, body, output_schema }`.
|
|
104
|
+
|
|
105
|
+
- `filter_config.body` renders to a boolean — the literal string
|
|
106
|
+
`"true"` (non-empty) means PASS; an empty render means SKIP.
|
|
107
|
+
(Same inverted-semantics rule as `interoperability_contracts.md`
|
|
108
|
+
§2 `filter_template`.)
|
|
109
|
+
- `decision_config.body` renders to a JSON array of decision keys.
|
|
110
|
+
Each rendered key fires the matching `action` (by
|
|
111
|
+
`action.decision_key`).
|
|
112
|
+
|
|
113
|
+
`output_schema` is a JSON-schema string that the platform uses
|
|
114
|
+
to validate the rendered output before invoking the next stage.
|
|
115
|
+
|
|
116
|
+
### Replace-on-PUT semantics for inline arrays
|
|
117
|
+
|
|
118
|
+
`actions`, `context_datasets`, and `ai_agents` (when present)
|
|
119
|
+
use **replace semantics on PUT**. Submitting an update body that
|
|
120
|
+
omits any of these arrays silently disassociates ALL of its
|
|
121
|
+
entries. Always re-supply the full array on every update, even
|
|
122
|
+
when you're only changing `description`.
|
|
123
|
+
|
|
124
|
+
Factor the workflow body construction into a helper that
|
|
125
|
+
accepts the changing fields and returns the full body — same
|
|
126
|
+
pattern used for datalake updates (see `datalakes.md` §5).
|
|
127
|
+
|
|
128
|
+
### `idempotency_template` prevents duplicate action firings
|
|
129
|
+
|
|
130
|
+
An action's `idempotency_template` renders to a string per
|
|
131
|
+
dataset row; the platform deduplicates so the same idempotency
|
|
132
|
+
key never fires twice. Typical recipe:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
'{{ <subject_id> }}-{{ <event_id> }}-{{ decision_key }}'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The placeholder names depend on the workflow's `dataset_type`
|
|
139
|
+
(patient_id + appointment.unregulated_appointment_id for an
|
|
140
|
+
appointment-driven workflow, customer_id + invoice.id for an
|
|
141
|
+
invoice-driven workflow, etc.).
|
|
142
|
+
|
|
143
|
+
### `trigger_template` controls scheduling
|
|
144
|
+
|
|
145
|
+
A Liquid template rendering to a datetime or one of these
|
|
146
|
+
literals:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
'now' — schedule at the next available slot at-or-after current time
|
|
150
|
+
'' — same as 'now' (empty string also routes to the now-slot)
|
|
151
|
+
ISO 8601 — schedule at the named instant
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The action is queued through the platform's scheduling layer
|
|
155
|
+
(action windows, throttling), so even `'now'` may resolve to a
|
|
156
|
+
slot some minutes in the future under load.
|
|
157
|
+
|
|
158
|
+
### Action-window + queue-throttle render `'now'` non-literal
|
|
159
|
+
|
|
160
|
+
Avoid templates like `{{ "" | now | date: "%Y-%m-%d..." }}` —
|
|
161
|
+
they render a literal current timestamp, which then collides
|
|
162
|
+
with the action-window clamp + queue throttle and pushes the
|
|
163
|
+
slot forward unpredictably. Use the literal `'now'` sentinel
|
|
164
|
+
when you want "fire as soon as possible".
|
|
165
|
+
|
|
166
|
+
## 3. Agent-driven workflows: AI enrichment + AI-emitted decisions
|
|
167
|
+
|
|
168
|
+
When `decision_config.type` references an AI agent (instead of
|
|
169
|
+
`'custom'` with a literal Liquid body), the workflow gains an
|
|
170
|
+
**enrichment** step between filter and decision:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
filter
|
|
174
|
+
│
|
|
175
|
+
▼
|
|
176
|
+
enrichment — each ai_agent runs, producing typed output
|
|
177
|
+
that the decision template can reference
|
|
178
|
+
│
|
|
179
|
+
▼
|
|
180
|
+
decision — AI agent emits the decision_key array
|
|
181
|
+
(or a custom decision template references
|
|
182
|
+
agent outputs via Liquid)
|
|
183
|
+
│
|
|
184
|
+
▼
|
|
185
|
+
action(s)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Bound agents are declared in `ai_agents: []` on the workflow
|
|
189
|
+
body. The full AI agent surface (create body, prompt config,
|
|
190
|
+
input_schema / llm_response_schema, runtime failure codes) lives
|
|
191
|
+
in `ai_agents.md`. The workflow-side embed shape is:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
ai_agents: [
|
|
195
|
+
{
|
|
196
|
+
ai_agent_id: aiAgentId, // UUID from api.aiAgents.create
|
|
197
|
+
position: 0, // ordering when multiple agents fire
|
|
198
|
+
context_mapping_config: {
|
|
199
|
+
type: 'custom',
|
|
200
|
+
// Liquid renders to a JSON object whose keys match the
|
|
201
|
+
// referenced agent's input_schema. The values reference
|
|
202
|
+
// the inbound dataset row's fields via `event_dataset.<field>`.
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
msg: '{{ event_dataset.message }}',
|
|
205
|
+
submission_id: '{{ event_dataset.submission_id }}',
|
|
206
|
+
}),
|
|
207
|
+
output_schema: '{"type":"object"}',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The agent's parsed output is exposed in the workflow's runtime
|
|
214
|
+
context at `additional_context.<agent_slug>.<field>` (property
|
|
215
|
+
paths match the agent's `llm_response_schema`). Decision and
|
|
216
|
+
action templates reference it via Liquid:
|
|
217
|
+
|
|
218
|
+
```liquid
|
|
219
|
+
{{ additional_context.contact-us-triage-categorizer.category }}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Standard (non-agent) workflows omit `ai_agents` — the enrichment
|
|
223
|
+
stage observably runs but emits `{ status: 'skipped' }` in the
|
|
224
|
+
per-step artifact (see §6).
|
|
225
|
+
|
|
226
|
+
### `event_dataset` is the workflow accessor for the inbound row
|
|
227
|
+
|
|
228
|
+
Workflow templates (filter, context mappings, action bodies)
|
|
229
|
+
reference the inbound dataset row via the `event_dataset`
|
|
230
|
+
variable. Field paths match the columns of the workflow's
|
|
231
|
+
`dataset_type`:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
dataset_type: 'appointment' → event_dataset.id, event_dataset.start, ...
|
|
235
|
+
dataset_type: 'generic_table' → event_dataset.<column_name> for each
|
|
236
|
+
column on the bound table
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For generic-table-bound workflows, `event_dataset` carries the
|
|
240
|
+
table's columns directly (no separate `row` indirection).
|
|
241
|
+
|
|
242
|
+
### `skip_mdm_resolution: true` for generic-table workflows
|
|
243
|
+
|
|
244
|
+
Workflows bound to a `dataset_type: 'generic_table'` typically
|
|
245
|
+
set `skip_mdm_resolution: true` on the workflow body. The flag
|
|
246
|
+
opts out of the platform's entity-resolution step on the inbound
|
|
247
|
+
row, which doesn't apply when the row IS the subject (no canonical
|
|
248
|
+
entity to resolve to). Workflows on entity-anchored datasets
|
|
249
|
+
(`patient`, `customer`, `legal_entity`, etc.) leave the flag at
|
|
250
|
+
its default `false`.
|
|
251
|
+
|
|
252
|
+
## 4. Field ownership
|
|
253
|
+
|
|
254
|
+
**Server-derived (Response-only).** Universal set from
|
|
255
|
+
`type_naming.md`, plus:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
actions[*].id UUID per inline action row
|
|
259
|
+
context_datasets[*].id UUID per inline context-dataset row
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Caller-supplied (round-trip).**
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
name required string
|
|
266
|
+
description optional string
|
|
267
|
+
dataset_type required string — the dataset this listens on
|
|
268
|
+
status required enum — 'live' | 'draft'
|
|
269
|
+
generic_table_id optional UUID — required iff dataset_type='generic_table'
|
|
270
|
+
skip_mdm_resolution optional bool — default false; set true for
|
|
271
|
+
generic-table-bound workflows
|
|
272
|
+
filter_config required embed — { type, body, output_schema }
|
|
273
|
+
decision_config required embed — same shape
|
|
274
|
+
context_datasets required array — inline; replace-on-PUT
|
|
275
|
+
actions required array — inline; replace-on-PUT
|
|
276
|
+
ai_agents optional array — inline; replace-on-PUT (§3)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Write-only (Request-only).** None.
|
|
280
|
+
|
|
281
|
+
## 5. Lifecycle
|
|
282
|
+
|
|
283
|
+
### Create
|
|
284
|
+
|
|
285
|
+
Synchronous. The workflow is immediately runnable when
|
|
286
|
+
`status: 'live'`. `status: 'draft'` is a save-without-arming
|
|
287
|
+
flag — drafts can be re-PUT but won't fire on dataset events.
|
|
288
|
+
|
|
289
|
+
### Read shapes
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
.list(tenantSlug, datalakeSlug) Paged: { data, meta }
|
|
293
|
+
.get(tenantSlug, datalakeSlug, idOrSlug) One row
|
|
294
|
+
.metadata(tenantSlug, datalakeSlug, idOrSlug) Agent-facing metadata
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Run
|
|
298
|
+
|
|
299
|
+
`.run` evaluates the workflow against a SQL-selected row set:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const { data: run } = await api.workflows.run(
|
|
303
|
+
tenantSlug, workflowSlug,
|
|
304
|
+
{
|
|
305
|
+
sql_where_clause: `ra.id = '${appointmentId}'`, // run against the regulated tables
|
|
306
|
+
mode: 'live', // 'live' | 'dry_run'
|
|
307
|
+
manual_override: false, // see below
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
// run.batch_id — tagged string (e.g. "manual:<uuid>");
|
|
311
|
+
// the prefix distinguishes manual runs from
|
|
312
|
+
// cron-driven ones
|
|
313
|
+
// run.workflow_run_log_id — UUID — the batch log row (poll via batchLogs)
|
|
314
|
+
// run.enqueued_count — number of rows the WHERE clause matched
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`sql_where_clause` accepts a fragment against the dataset's
|
|
318
|
+
regulated tables (`ra` for appointments, `rp` for patients,
|
|
319
|
+
etc. — same alias scheme as `data_activation_clients.md` §6.5).
|
|
320
|
+
The id column is ambiguous across joins; always prefix
|
|
321
|
+
(`ra.id`, `rp.id`).
|
|
322
|
+
|
|
323
|
+
`manual_override: true` **skips BOTH the filter expression AND
|
|
324
|
+
the idempotency check** — the workflow fires regardless of
|
|
325
|
+
filter outcome and regardless of whether the idempotency tuple
|
|
326
|
+
already fired for this row. Useful for forced re-runs in test
|
|
327
|
+
contexts; avoid in production.
|
|
328
|
+
|
|
329
|
+
### Execute (per-row, per-action)
|
|
330
|
+
|
|
331
|
+
`.execute` skips filter + decision evaluation entirely and
|
|
332
|
+
fires one specific action against one specific dataset row:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const { data: result } = await api.workflows.execute(
|
|
336
|
+
tenantSlug, workflowSlug,
|
|
337
|
+
{
|
|
338
|
+
dataset_id: unregulatedAppointmentId, // UNREGULATED id (not regulated)
|
|
339
|
+
decision_key: 'send_appointment_review_sms',
|
|
340
|
+
mode: 'live', // 'live' | 'dry_run' (same enum as .run)
|
|
341
|
+
manual_override: true, // load-bearing on re-fires
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
// result.status — 'pending' | 'completed' | 'filtered' | 'failed'
|
|
345
|
+
// result.scheduled_count — number of action jobs scheduled
|
|
346
|
+
// result.workflow_execution_log_id — UUID of the execution-log row
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Note that `dataset_id` is the **unregulated** id, not the
|
|
350
|
+
regulated one. The unregulated tier is what the workflow
|
|
351
|
+
runtime reads its action context from; passing the regulated
|
|
352
|
+
id will fail to resolve. Issue a `dataAccessMode: 'unregulated'`
|
|
353
|
+
search to obtain the right id.
|
|
354
|
+
|
|
355
|
+
`manual_override: true` on `.execute` sets the unique-key
|
|
356
|
+
constraint to `false` on the per-row job, allowing re-fires
|
|
357
|
+
on rows that have already been processed (otherwise the new
|
|
358
|
+
log row sits in `:pending` forever, deduplicated by the
|
|
359
|
+
sampled-event uniqueness check).
|
|
360
|
+
|
|
361
|
+
### Update
|
|
362
|
+
|
|
363
|
+
`PUT` replays the full body — no PATCH. Replace-on-PUT applies
|
|
364
|
+
to all inline arrays (see §2).
|
|
365
|
+
|
|
366
|
+
### Delete
|
|
367
|
+
|
|
368
|
+
`DELETE` removes the workflow. In-flight runs decouple their
|
|
369
|
+
lifecycle from the workflow row; deleting one mid-run rejects
|
|
370
|
+
with a 422.
|
|
371
|
+
|
|
372
|
+
## 6. Run-time logs
|
|
373
|
+
|
|
374
|
+
### Batch logs — `api.workflows.batchLogs`
|
|
375
|
+
|
|
376
|
+
One row per workflow run (per `.run` call). Lifecycle:
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
:pending → :completed | :failed | :partial
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
- `:completed` — every row processed terminally
|
|
383
|
+
- `:failed` — every row's processing errored
|
|
384
|
+
- `:partial` — mixed (some rows succeeded, some filtered, some failed)
|
|
385
|
+
|
|
386
|
+
Methods:
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
.list(tenantSlug, workflowSlug) Paged
|
|
390
|
+
.get(tenantSlug, workflowSlug, runLogId) One row
|
|
391
|
+
.refresh(tenantSlug, workflowSlug, runLogId) Force a status recompute
|
|
392
|
+
(per-row jobs are async; status
|
|
393
|
+
only flips after explicit refresh)
|
|
394
|
+
.start(tenantSlug, workflowSlug, runLogId) Begin polling for this batch
|
|
395
|
+
.stop(tenantSlug, workflowSlug, runLogId) Stop polling
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The `.refresh` verb is **load-bearing** for synchronous
|
|
399
|
+
progress checks — `.get` returns the cached status, but the
|
|
400
|
+
underlying per-row jobs may have completed since the last
|
|
401
|
+
refresh. Call `.refresh` in your poll loop.
|
|
402
|
+
|
|
403
|
+
### Workflow execution logs — `api.workflows.workflowLogs`
|
|
404
|
+
|
|
405
|
+
One row per dataset row evaluated. Status enum:
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
:pending → :executing → :completed | :filtered | :failed
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Each log carries:
|
|
412
|
+
|
|
413
|
+
- `id`, `workflow_id`, `batch_id` — join keys
|
|
414
|
+
- `status` — terminal state
|
|
415
|
+
- `actions_total` — number of actions wired on the workflow
|
|
416
|
+
- `actions_pending` — actions scheduled but not yet completed by
|
|
417
|
+
their per-action workers
|
|
418
|
+
- `actions_completed` — actions whose worker has finished
|
|
419
|
+
- `actions_skipped` — siblings of the matched action that didn't
|
|
420
|
+
fire (the decision template returned one key; other actions
|
|
421
|
+
with non-matching `decision_key` get :skipped)
|
|
422
|
+
- `error_message` — coarse failure summary (e.g.
|
|
423
|
+
`"AI enrichment failed: <agent_name>"`); rich detail lives
|
|
424
|
+
in `error.json` (§ Per-step artifacts below)
|
|
425
|
+
- `action_execution_logs` — nested rows, one per action that fired
|
|
426
|
+
(`message_body` virtual field is rendered when read via the
|
|
427
|
+
`dataAccessMode: 'regulated'` mode)
|
|
428
|
+
|
|
429
|
+
Methods:
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
.list(tenantSlug, workflowSlug) Paged
|
|
433
|
+
.get(tenantSlug, workflowSlug, logId, { dataAccessMode? })
|
|
434
|
+
.download(tenantSlug, workflowSlug, logId) Downloads the run's artifact
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The `dataAccessMode` query parameter on `.get` controls how
|
|
438
|
+
`message_body` is rendered:
|
|
439
|
+
- `'regulated'` — raw rendered body (with the resolved short
|
|
440
|
+
path `/t/<token>`)
|
|
441
|
+
- `'unregulated'` — tokenised display body
|
|
442
|
+
|
|
443
|
+
### Per-step artifacts (event, filter, enrichment, error)
|
|
444
|
+
|
|
445
|
+
For each workflow execution, the platform writes JSON artifacts
|
|
446
|
+
to the datalake's regulated cloud-storage bucket. The keys
|
|
447
|
+
follow a fixed convention:
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
workflows/<workflow_id>/executions/<execution_log_id>/<step>.json
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Where `<step>` is one of: `event` | `filter` | `enrichment` |
|
|
454
|
+
`error`.
|
|
455
|
+
|
|
456
|
+
Fetch them via the datalake's `createDownloadLink`:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
const { data: link } = await api.datalakes.createDownloadLink(
|
|
460
|
+
tenantSlug, datalakeSlug,
|
|
461
|
+
{ bucket: regulatedBucket, key: 'workflows/.../filter.json' },
|
|
462
|
+
)
|
|
463
|
+
const artifact = await (await fetch(link.url)).json()
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Artifact bodies:
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
event.json { event_dataset: { ... full inbound row snapshot ... } }
|
|
470
|
+
filter.json { filter_expression, filter_result: boolean }
|
|
471
|
+
enrichment.json Success-with-agents:
|
|
472
|
+
{
|
|
473
|
+
status: 'completed',
|
|
474
|
+
<agent_slug_1>: { status: 'completed',
|
|
475
|
+
output: { ...matches llm_response_schema... } },
|
|
476
|
+
<agent_slug_2>: { ... },
|
|
477
|
+
}
|
|
478
|
+
Skipped (no ai_agents on the workflow):
|
|
479
|
+
{ status: 'skipped' }
|
|
480
|
+
error.json Only present on :failed runs:
|
|
481
|
+
{
|
|
482
|
+
error_code: string,
|
|
483
|
+
stage: string, // 'enrichment' | 'decision' | 'action'
|
|
484
|
+
ai_agent_slug?: string, // present for enrichment failures
|
|
485
|
+
error_message: string, // coarse summary (mirrors WEL.error_message)
|
|
486
|
+
detail: string, // rich provider context (URL, HTTP status, ...)
|
|
487
|
+
// ONLY in error.json, NOT mirrored to the WEL row
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The PASS / REJECT branches of the filter both emit
|
|
492
|
+
`filter.json` — the body just differs in `filter_result`.
|
|
493
|
+
|
|
494
|
+
### `error.json` error codes
|
|
495
|
+
|
|
496
|
+
When `stage: 'enrichment'`, the agent-related codes are
|
|
497
|
+
documented in `ai_agents.md` §7. The full set of `error_code`
|
|
498
|
+
values across stages:
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
tool_execution_failed enrichment LLM tool's HTTP call failed
|
|
502
|
+
json_decode_failed enrichment provider returned non-JSON
|
|
503
|
+
context_mapping_failed enrichment mapping output didn't match agent's input_schema
|
|
504
|
+
ai_agent_disabled enrichment agent's enabled: false
|
|
505
|
+
ai_agent_not_found enrichment agent was deleted but workflow still references it
|
|
506
|
+
decision_template_failed decision Liquid render or schema validation failed
|
|
507
|
+
action_template_failed action action body Liquid render failed
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
The `detail` field carries rich context (URL, HTTP status,
|
|
511
|
+
transport error) that lives ONLY in `error.json`. The
|
|
512
|
+
workflow execution log row's `error_message` is intentionally
|
|
513
|
+
coarse so the database column doesn't grow with arbitrary
|
|
514
|
+
provider responses. Always read `error.json` for diagnostics.
|
|
515
|
+
|
|
516
|
+
## 7. Connected apps as action sinks
|
|
517
|
+
|
|
518
|
+
Workflows that send messages (SMS, email) typically link to a
|
|
519
|
+
**connected app** — a registered consumer of the action's
|
|
520
|
+
rendered URL. See `api.connectedApps`:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
await api.connectedApps.create(tenantSlug, datalakeSlug, {
|
|
524
|
+
name: 'Review form',
|
|
525
|
+
description: '...',
|
|
526
|
+
mode: 'self_hosted', // 'self_hosted' | 'managed'
|
|
527
|
+
urls: [{ url: 'https://app.example.com', is_primary: true, label: 'production' }],
|
|
528
|
+
})
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Action wiring on the workflow:
|
|
532
|
+
- `connected_app_id` — UUID of the bound connected app
|
|
533
|
+
- `connected_app_route` — path on the app (e.g. `/forms/review`)
|
|
534
|
+
- `connected_app_metadata_template` — Liquid template rendering
|
|
535
|
+
a JSON object that the recipient can introspect after
|
|
536
|
+
resolving the token
|
|
537
|
+
|
|
538
|
+
At fire time, the action's message body's `{{ connected_app_form_url }}`
|
|
539
|
+
expands to `https://<app-primary>/t/<short_path>`. The recipient
|
|
540
|
+
hits that URL; the connected app calls
|
|
541
|
+
`api.connectedApps.resolvePage` with the `short_path` to fetch
|
|
542
|
+
the metadata, then `api.connectedApps.updateMessageTracking` to
|
|
543
|
+
record `opened_at` / `form_submitted_at`.
|
|
544
|
+
|
|
545
|
+
## 8. Gotchas
|
|
546
|
+
|
|
547
|
+
1. **`filter_config` follows the same inverted-render rule as
|
|
548
|
+
interoperability contracts: non-empty PASSES, empty SKIPS.**
|
|
549
|
+
Same wording, same trap. See
|
|
550
|
+
`interoperability_contracts.md` §2 for the worked example.
|
|
551
|
+
|
|
552
|
+
2. **Replace-on-PUT silently drops omitted arrays.** Workflow
|
|
553
|
+
updates must re-supply `actions`, `context_datasets`, and
|
|
554
|
+
`ai_agents` verbatim — otherwise the existing rows are
|
|
555
|
+
deleted. Factor the body construction so create and update
|
|
556
|
+
share one builder.
|
|
557
|
+
|
|
558
|
+
3. **`sql_where_clause` ids require an alias prefix.** `id` is
|
|
559
|
+
ambiguous across the multi-JOIN base query; use
|
|
560
|
+
`<base_alias>.id` (`ra.id`, `rp.id`, `rc.id`, etc.).
|
|
561
|
+
|
|
562
|
+
4. **`.execute` takes the UNREGULATED dataset id, `.run` takes
|
|
563
|
+
the REGULATED id.** This asymmetry traps consumers who
|
|
564
|
+
reuse the id they got from a regulated-mode search. Issue
|
|
565
|
+
a fresh search with `dataAccessMode: 'unregulated'` before
|
|
566
|
+
calling `.execute`.
|
|
567
|
+
|
|
568
|
+
5. **`manual_override: true` on `.execute` is required for
|
|
569
|
+
re-fires.** Without it, the per-row job dedupes against the
|
|
570
|
+
sampled-event uniqueness check and the new log row sits in
|
|
571
|
+
`:pending` indefinitely. The override flips the job's
|
|
572
|
+
unique-constraint behavior so the re-fire is allowed.
|
|
573
|
+
|
|
574
|
+
6. **`batchLogs.get` returns cached status; call `.refresh`.**
|
|
575
|
+
Per-row jobs are async; `.get` does not recompute. Poll
|
|
576
|
+
`.refresh(runLogId)` in your wait loop.
|
|
577
|
+
|
|
578
|
+
7. **`trigger_template: 'now'` (literal) is different from
|
|
579
|
+
`'{{ "" | now | date: ... }}'`.** The literal sentinel
|
|
580
|
+
routes to the next available scheduling slot directly;
|
|
581
|
+
the rendered timestamp gets clamped against the action
|
|
582
|
+
window + queue throttle and pushed forward unpredictably.
|
|
583
|
+
|
|
584
|
+
8. **Enrichment runs for every workflow, even without agents.**
|
|
585
|
+
For standard (non-agent) workflows, `enrichment.json` lands
|
|
586
|
+
with `{ status: 'skipped' }`. Don't treat the artifact's
|
|
587
|
+
absence as the "no AI" signal — read its body.
|
|
588
|
+
|
|
589
|
+
9. **Use `| to_json`, NEVER `| json`, in Liquid bodies.** When
|
|
590
|
+
serializing nested values into a JSON payload (most commonly
|
|
591
|
+
in an agent's `context_mapping_config`, but also any
|
|
592
|
+
TemplateConfig body that must produce valid JSON), the
|
|
593
|
+
correct filter is `| to_json`. `| json` is NOT in the
|
|
594
|
+
platform's allowlist of custom filters (see `ai_sandbox.md`
|
|
595
|
+
§2 for the closed eleven-filter set). Using it produces
|
|
596
|
+
structurally-invalid JSON output silently — the downstream
|
|
597
|
+
JSON parse step then rejects the rendered string with a
|
|
598
|
+
generic decode error far from the Liquid template's location,
|
|
599
|
+
making it expensive to diagnose. Always write
|
|
600
|
+
`{{ event_dataset | to_json }}`, not
|
|
601
|
+
`{{ event_dataset | json }}`.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# `@alvera-ai/platform-sdk`
|
|
2
|
+
|
|
3
|
+
Typed SDK for the Alvera platform API — manage datalakes, data sources,
|
|
4
|
+
tools, AI agents, action status updaters, and more.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @alvera-ai/platform-sdk
|
|
10
|
+
# or: pnpm add / bun add / yarn add
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { AlveraClient } from '@alvera-ai/platform-sdk'
|
|
17
|
+
|
|
18
|
+
const alvera = new AlveraClient({ baseUrl, sessionToken })
|
|
19
|
+
const { data } = await alvera.datalakes.list(tenantSlug)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Agent setup
|
|
23
|
+
|
|
24
|
+
This package ships an agent-docs corpus under `.agent/` (it lands at
|
|
25
|
+
`node_modules/@alvera-ai/platform-sdk/.agent/` after install). Coding
|
|
26
|
+
agents that walk `node_modules` discover it automatically.
|
|
27
|
+
|
|
28
|
+
To wire a pointer into your project's `AGENTS.md` (and a `@AGENTS.md`
|
|
29
|
+
import into `CLAUDE.md`), run — no global install needed:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npx @alvera-ai/platform-sdk llm-export
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It writes an idempotent managed block; re-running replaces only that
|
|
36
|
+
block. Start from the corpus index: `.agent/AGENTS.md`.
|
|
37
|
+
|
|
38
|
+
## See also
|
|
39
|
+
|
|
40
|
+
- [Root README](../../README.md) — cross-repo overview (platform · SDK · CLI)
|
|
41
|
+
- [`packages/cli/README.md`](../cli/README.md) — the `alvera` CLI verb surface
|
|
42
|
+
- [`docs/cli-architecture.md`](../../docs/cli-architecture.md) — CLI design canon
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
Elastic-2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|