@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
package/.agent/templates.md
CHANGED
|
@@ -4,9 +4,8 @@ Templates are an **INFRASTRUCTURE** building block: the platform
|
|
|
4
4
|
ships a library of Liquid templates that other infrastructure
|
|
5
5
|
resources consume — interoperability contracts, AI agent prompts,
|
|
6
6
|
workflow definitions, status poller bodies. Templates are
|
|
7
|
-
**read-only** from the SDK
|
|
8
|
-
|
|
9
|
-
surface in its catalog.
|
|
7
|
+
**read-only** from the SDK — they ship with the platform and a
|
|
8
|
+
datalake's catalog is whatever the platform exposes to it.
|
|
10
9
|
|
|
11
10
|
SDK namespace: `api.templates`.
|
|
12
11
|
|
|
@@ -21,14 +20,15 @@ Three discovery methods, two complementary response shapes —
|
|
|
21
20
|
structured (for code) and markdown (for agents):
|
|
22
21
|
|
|
23
22
|
```typescript
|
|
24
|
-
|
|
23
|
+
// The systemTemplates catalog response has no exported type on the
|
|
24
|
+
// public surface yet — `catalog` is inferred from the call.
|
|
25
25
|
|
|
26
26
|
// Structured catalog
|
|
27
27
|
const { data: catalog } = await api.templates.systemTemplates(
|
|
28
28
|
tenantSlug,
|
|
29
29
|
datalakeSlug,
|
|
30
30
|
)
|
|
31
|
-
// catalog.data
|
|
31
|
+
// catalog.data — the full in-scope list, NOT paginated
|
|
32
32
|
// { path, content, output_schema }
|
|
33
33
|
// path category/sub-category/name segment string
|
|
34
34
|
// content Liquid template body
|
|
@@ -59,10 +59,12 @@ const { data: templateMd } = await api.templates.metadataDetails(
|
|
|
59
59
|
Template paths follow `<category>/<sub-category>/<name>`.
|
|
60
60
|
Categories the SDK exposes include
|
|
61
61
|
`data_activation/interoperability/`, `ai_agents/`, `workflows/`,
|
|
62
|
-
`status_poller/`.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
`status_poller/`. Path segments below the category are platform-owned and have
|
|
63
|
+
outlived at least one naming scheme, so **discover paths via
|
|
64
|
+
`api.templates` rather than constructing them.** The
|
|
65
|
+
`data_domain` filter that used to select a datalake's slice of
|
|
66
|
+
the catalog was removed with GH-859; what remains is
|
|
67
|
+
server-decided.
|
|
66
68
|
|
|
67
69
|
## 3. Lifecycle
|
|
68
70
|
|
|
@@ -84,10 +86,9 @@ trying to add to the catalog.
|
|
|
84
86
|
`'data_activation_interoperability'`). Check the typed
|
|
85
87
|
parameter for the valid set; multiple templates can share a
|
|
86
88
|
basename across intents. Within a single intent, the basename
|
|
87
|
-
must resolve uniquely under its `<intent
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`data_activation/interoperability/<industry>/`),
|
|
89
|
+
must resolve uniquely under its `<intent>` prefix — if two
|
|
90
|
+
sub-directories under that prefix ship the same basename (e.g.
|
|
91
|
+
two vendors under `data_activation/interoperability/`),
|
|
91
92
|
`metadataDetails` returns `409 Conflict` with the candidate
|
|
92
93
|
paths; a missing basename returns `404`.
|
|
93
94
|
|
package/.agent/tools.md
CHANGED
|
@@ -116,17 +116,27 @@ mistake with a consequence: a `rest_api` Twilio sender has no
|
|
|
116
116
|
happened to it** — no delivery status, no reconciliation.
|
|
117
117
|
|
|
118
118
|
```typescript
|
|
119
|
+
import { ToolIntent } from '@alvera-ai/platform-sdk'
|
|
120
|
+
|
|
119
121
|
await api.tools.create(tenantSlug, datalakeSlug, {
|
|
120
122
|
name: 'Campaign Sender — Main Line',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
description: 'Twilio sender for the campaign workflow.',
|
|
124
|
+
intent: ToolIntent.SMS,
|
|
125
|
+
status: 'active',
|
|
126
|
+
datalake_id: datalakeId,
|
|
127
|
+
// The provider-specific fields are NESTED under `body`, discriminated by
|
|
128
|
+
// `tool_body_type` — they are not top-level keys on the create request.
|
|
129
|
+
// Flattening them is the most common 422 on this endpoint.
|
|
130
|
+
body: {
|
|
131
|
+
tool_body_type: 'twilio',
|
|
132
|
+
variant_type: 'primary',
|
|
133
|
+
account_sid: 'AC…', // "AC" + 32 hex
|
|
134
|
+
auth_token: '…', // writeOnly — never comes back on a read
|
|
135
|
+
from_number: '+15550000001', // XOR with messaging_service_sid
|
|
136
|
+
// base_url — nullable; blank means https://api.twilio.com
|
|
137
|
+
// timeout_ms — 1..300000, nullable
|
|
138
|
+
// base_message — ComplexTemplateConfig
|
|
139
|
+
},
|
|
130
140
|
})
|
|
131
141
|
```
|
|
132
142
|
|
|
@@ -627,13 +637,34 @@ await api.tools.testInvocation(
|
|
|
627
637
|
inlines the value verbatim; `type: 'system'` references a
|
|
628
638
|
platform-shipped template (see `templates.md`).
|
|
629
639
|
|
|
630
|
-
**Test-invocation accepts
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
`
|
|
634
|
-
`
|
|
635
|
-
|
|
636
|
-
|
|
640
|
+
**Test-invocation accepts SIX `tool_call_type` variants** — the
|
|
641
|
+
interactive ones. Its request casts into a `ManualToolInvocation` embed
|
|
642
|
+
(`on_type_not_found: :raise`) whose polymorphic set is `sms_request`,
|
|
643
|
+
`mms_request`, `email_request`, `restapi_request`, `aws_lambda_request`
|
|
644
|
+
and `sql_query`.
|
|
645
|
+
|
|
646
|
+
THREE LISTS LIVE HERE and conflating them is the mistake this paragraph
|
|
647
|
+
used to make. `tool_body_type` says what a tool IS (12 variants — `sns`,
|
|
648
|
+
`twilio`, `s3`, `sql_database`, `sftp`, `sharepoint`, `manual_upload`, …).
|
|
649
|
+
`tool_call_type` says how a CALL is shaped (9). Test-invocation accepts a
|
|
650
|
+
subset of the call types (6). They do not map one-to-one: `sns` and
|
|
651
|
+
`twilio` are both bodies that send SMS, so both call through
|
|
652
|
+
`sms_request`; the `sql_database` body calls through `sql_query`.
|
|
653
|
+
|
|
654
|
+
So a tool whose body is **S3, SFTP or SharePoint** has no test-invocation
|
|
655
|
+
variant and cannot be exercised through this endpoint (same shape as the
|
|
656
|
+
`status_poller` case below). SFTP and SharePoint are coming; S3 is not
|
|
657
|
+
currently reachable this way either.
|
|
658
|
+
|
|
659
|
+
**`manual_upload` is the exception worth knowing.** It is excluded from
|
|
660
|
+
test-invocation, but it is NOT unexecutable — it has a real executor
|
|
661
|
+
(`Platform.Tools.ManualUpload`, protocols.ex:105) which routes to the
|
|
662
|
+
`ingest_json` path rather than the automated fetch pipeline. There is
|
|
663
|
+
nothing to dial interactively because the input is a file someone
|
|
664
|
+
supplies; the tool still runs. A born-red contract that demands a
|
|
665
|
+
test-invocation smoke for a manual-upload tool is therefore asking for
|
|
666
|
+
something that cannot exist, and the cure is to assert the tool's
|
|
667
|
+
configuration and its provenance binding instead.
|
|
637
668
|
Those config shapes still exist — but on the broader **workflow-action
|
|
638
669
|
`ToolCallConfig`** surface (see `tool-call-configs.md`), not here. The
|
|
639
670
|
per-branch required-list for the five test-invocable variants:
|
|
@@ -661,10 +692,18 @@ per-branch required-list for the five test-invocable variants:
|
|
|
661
692
|
|
|
662
693
|
tool_call_type: 'restapi_request'
|
|
663
694
|
required: path, method, pagination_context_template
|
|
664
|
-
enums: method ∈
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
695
|
+
enums: method ∈ 'head' | 'get' | 'put' | 'post' | 'delete' |
|
|
696
|
+
'patch' — LOWERCASE. 'POST' is rejected as
|
|
697
|
+
`/tool_call/method — Invalid value for enum`.
|
|
698
|
+
optional: body, params
|
|
699
|
+
shapes: path, body, params and pagination_context_template are
|
|
700
|
+
EACH a TemplateConfig ({ type, body }) — 'identity' or
|
|
701
|
+
'custom' both accepted. `path` is NOT a plain string;
|
|
702
|
+
passing one is `/tool_call/path — Invalid value: Invalid
|
|
703
|
+
object. Got: string`. It is an envelope so a path can be
|
|
704
|
+
interpolated per row.
|
|
705
|
+
pagination_context_template is required even with nothing
|
|
706
|
+
to paginate — `{ type: 'null' }` is how you say so.
|
|
668
707
|
|
|
669
708
|
tool_call_type: 'aws_lambda_request'
|
|
670
709
|
required: payload (TemplateConfig — 'identity' acceptable)
|
|
@@ -714,9 +753,12 @@ that is the cheapest way to prove its credentials:
|
|
|
714
753
|
await api.tools.testInvocation(tenantSlug, datalakeSlug, pollerId, {
|
|
715
754
|
tool_call: {
|
|
716
755
|
tool_call_type: 'restapi_request',
|
|
717
|
-
method: 'get',
|
|
756
|
+
method: 'get' as const,
|
|
718
757
|
path: { type: 'custom', body: '/v3/YOUR-DOMAIN/events' },
|
|
719
758
|
params: { type: 'custom', body: '{"limit": 1}' }, // keep it read-only
|
|
759
|
+
// REQUIRED, even for a one-shot probe: a REST call must always say
|
|
760
|
+
// whether there is another page. Omit it and the create is refused.
|
|
761
|
+
pagination_context_template: { type: 'custom', body: '{"has_next": false}' },
|
|
720
762
|
},
|
|
721
763
|
})
|
|
722
764
|
```
|
package/.agent/type_naming.md
CHANGED
|
@@ -91,26 +91,26 @@ The canonical pattern — capture both returned identifiers and use
|
|
|
91
91
|
each for the calls it keys:
|
|
92
92
|
|
|
93
93
|
```typescript
|
|
94
|
-
// 1. create —
|
|
94
|
+
// 1. create — every response carries `id`; only SOME carry `slug`.
|
|
95
95
|
const { data: created } = await api.tools.create(
|
|
96
96
|
tenantSlug, datalakeSlug, body,
|
|
97
97
|
)
|
|
98
|
-
const toolId = created.id
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
const toolId = created.id! // <-- CRUD + cross-resource refs
|
|
99
|
+
|
|
100
|
+
// A tool has NO slug, and neither does a generic table — they are
|
|
101
|
+
// addressed by id and by name respectively. The slug-bearing responses
|
|
102
|
+
// are the ones whose ops verbs take a slug in the path: datalakes,
|
|
103
|
+
// workflows, data activation clients, connected apps, AI agents and
|
|
104
|
+
// interoperability contracts. Reach for `.slug` only on those, and
|
|
105
|
+
// note every id and slug is optional on the wire, so a strict consumer
|
|
106
|
+
// needs the `!` (or a guard) that the cookbooks use.
|
|
101
107
|
|
|
102
108
|
// 2a. CRUD reads/updates key on the UUID id
|
|
103
109
|
await api.tools.get(tenantSlug, datalakeSlug, toolId)
|
|
104
110
|
|
|
105
|
-
// 2b. downstream resources reference the tool by
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
action_log_config: {
|
|
109
|
-
type: 'custom',
|
|
110
|
-
body: '{"external_id": "{{ notification.messageId }}", "status": "delivered"}',
|
|
111
|
-
},
|
|
112
|
-
// …
|
|
113
|
-
})
|
|
111
|
+
// 2b. downstream resources reference the tool by ID, never by name or
|
|
112
|
+
// slug — see action_status_updaters.md for the full body:
|
|
113
|
+
// updater_tool_id: toolId
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
Never:
|
package/.agent/workflows.md
CHANGED
|
@@ -37,8 +37,8 @@ connected apps, AI agents) must exist before you `create()`.
|
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
39
|
import type {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
AgenticWorkflowRequestWritable,
|
|
41
|
+
AgenticWorkflowResponse,
|
|
42
42
|
} from '@alvera-ai/platform-sdk'
|
|
43
43
|
|
|
44
44
|
const { data: created } = await api.workflows.create(
|
|
@@ -47,12 +47,23 @@ const { data: created } = await api.workflows.create(
|
|
|
47
47
|
{
|
|
48
48
|
name: 'Appointment Review SMS',
|
|
49
49
|
description: 'Send a review-request SMS after a fulfilled appointment',
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
// The workflow listens on ONE generic table, named by id. The
|
|
52
|
+
// platform's own dataset types (legal_entity, message,
|
|
53
|
+
// action_log, ...) are the other legal values — see §4.
|
|
54
|
+
dataset_type: 'generic_table',
|
|
55
|
+
generic_table_id: appointmentsTableId,
|
|
56
|
+
|
|
57
|
+
// Load-bearing whenever an action mints a per-subject
|
|
58
|
+
// `connected_app_form_url`: resolution is what attaches the row
|
|
59
|
+
// to its legal entity. See §2.
|
|
60
|
+
skip_mdm_resolution: false,
|
|
61
|
+
|
|
51
62
|
status: 'live', // 'live' | 'draft' | 'manual' (see §5)
|
|
52
63
|
tags: ['appointments', 'sms'], // REQUIRED on every write — [] if untagged (see §2)
|
|
53
64
|
filter_config: {
|
|
54
65
|
type: 'custom',
|
|
55
|
-
body:
|
|
66
|
+
body: '{% if event_dataset.status == "fulfilled" %}true{% endif %}',
|
|
56
67
|
// NO output_schema here — filter_config takes { type, body } only.
|
|
57
68
|
// See §2: output_schema is server-pinned for filter_config, not a
|
|
58
69
|
// request field.
|
|
@@ -65,30 +76,37 @@ const { data: created } = await api.workflows.create(
|
|
|
65
76
|
context_datasets: [
|
|
66
77
|
{
|
|
67
78
|
dataset_type: 'message',
|
|
68
|
-
where_clause:
|
|
79
|
+
where_clause:
|
|
80
|
+
`m.legal_entity_id = '{{ legal_entity_id }}' ` +
|
|
81
|
+
`AND m.sent_at > NOW() - INTERVAL '6 months'`,
|
|
69
82
|
limit: 1,
|
|
70
83
|
position: 0,
|
|
71
84
|
},
|
|
72
85
|
],
|
|
73
86
|
actions: [
|
|
74
87
|
{
|
|
75
|
-
action_type:
|
|
88
|
+
action_type: ActionType.SMS,
|
|
76
89
|
tool_id: smsToolId,
|
|
77
90
|
decision_key: 'send_appointment_review_sms',
|
|
78
91
|
position: 0,
|
|
79
92
|
trigger_template: 'now', // 'now' = next available slot
|
|
80
93
|
action_window_start: 9, // send only 09:00–20:59 in the
|
|
81
94
|
action_window_end: 21, // datalake tz (SMS quiet-hours failsafe; see §2)
|
|
95
|
+
|
|
96
|
+
// Row identity for a generic_table workflow is the event
|
|
97
|
+
// row's own unique column. There is no {{ subject_id }} —
|
|
98
|
+
// see §2, and note an unknown binding renders EMPTY.
|
|
82
99
|
idempotency_template:
|
|
83
|
-
'{{
|
|
100
|
+
'{{ event_dataset.appointment_id }}-{{ decision_key }}',
|
|
101
|
+
|
|
84
102
|
connected_app_id: connectedAppId,
|
|
85
103
|
connected_app_route: '/forms/review',
|
|
86
104
|
connected_app_metadata_template:
|
|
87
|
-
'{"appointment_id":"{{
|
|
105
|
+
'{"appointment_id":"{{ event_dataset.appointment_id }}"}',
|
|
88
106
|
tool_call: {
|
|
89
107
|
tool_call_type: 'sms_request',
|
|
90
|
-
to: { type: 'custom', body: '{{
|
|
91
|
-
body: { type: 'custom', body: 'Hi {{
|
|
108
|
+
to: { type: 'custom', body: '{{ event_dataset.patient_mobile | e164 }}' },
|
|
109
|
+
body: { type: 'custom', body: 'Hi {{ event_dataset.patient_name | split: " " | first }}, please share your feedback at {{ connected_app_form_url }}' },
|
|
92
110
|
sms_type: 'transactional',
|
|
93
111
|
},
|
|
94
112
|
},
|
|
@@ -176,13 +194,21 @@ dataset row; the platform deduplicates so the same idempotency
|
|
|
176
194
|
key never fires twice. Typical recipe:
|
|
177
195
|
|
|
178
196
|
```
|
|
179
|
-
'{{ <
|
|
197
|
+
'{{ <row identity> }}-{{ action_id }}-{{ decision_key }}'
|
|
180
198
|
```
|
|
181
199
|
|
|
182
|
-
The
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
The row identity depends on the workflow's `dataset_type`. For a
|
|
201
|
+
`generic_table` workflow it is the event row's unique column —
|
|
202
|
+
`{{ event_dataset.submission_id }}`, `{{ event_dataset.customer_number }}`;
|
|
203
|
+
for a `legal_entity` workflow the row IS the subject, so it is
|
|
204
|
+
`{{ legal_entity.id }}`.
|
|
205
|
+
|
|
206
|
+
**There is no `{{ subject_id }}`.** GH-859 removed it along with the
|
|
207
|
+
per-domain subjects (`patient_id`, `customer_id`, `party_id`), and it is a
|
|
208
|
+
trap rather than merely a rename: an unknown binding renders EMPTY instead of
|
|
209
|
+
failing, so a template still using one produces a key that is missing the very
|
|
210
|
+
component it exists to key on — and nothing anywhere says so. If two rows that
|
|
211
|
+
should dedupe are firing twice, look here first.
|
|
186
212
|
|
|
187
213
|
### Two scheduling levers, and they answer different questions
|
|
188
214
|
|
|
@@ -358,9 +384,10 @@ are removed, and re-supplied ones are repositioned.
|
|
|
358
384
|
```typescript
|
|
359
385
|
await api.workflows.create(tenantSlug, datalakeSlug, {
|
|
360
386
|
name: 'Contact-Us Triage',
|
|
361
|
-
|
|
387
|
+
description: 'Triages inbound contact-us messages.',
|
|
388
|
+
dataset_type: 'generic_table' as const,
|
|
389
|
+
status: 'live' as const,
|
|
362
390
|
tags: [],
|
|
363
|
-
// …other workflow fields…
|
|
364
391
|
workflow_ai_agents: [
|
|
365
392
|
{
|
|
366
393
|
ai_agent_id: aiAgentId, // UUID from api.aiAgents.create
|
|
@@ -402,7 +429,7 @@ variable. Field paths match the columns of the workflow's
|
|
|
402
429
|
`dataset_type`:
|
|
403
430
|
|
|
404
431
|
```
|
|
405
|
-
dataset_type: '
|
|
432
|
+
dataset_type: 'legal_entity' → event_dataset.id, event_dataset.name, ...
|
|
406
433
|
dataset_type: 'generic_table' → event_dataset.<column_name> for each
|
|
407
434
|
column on the bound table
|
|
408
435
|
```
|
|
@@ -534,7 +561,7 @@ for a time, or at `scheduled_at` if you did.
|
|
|
534
561
|
const { data: run } = await api.workflows.run(
|
|
535
562
|
tenantSlug, datalakeSlug, workflowSlug,
|
|
536
563
|
{
|
|
537
|
-
sql_where_clause: `
|
|
564
|
+
sql_where_clause: `le.id = '${legalEntityId}'`, // fragment against the dataset's alias
|
|
538
565
|
mode: 'live', // 'live' | 'dry_run'
|
|
539
566
|
manual_override: false, // see below
|
|
540
567
|
scheduled_at: '2026-08-12T09:00:00+01:00', // OPTIONAL — see below
|
|
@@ -616,11 +643,17 @@ worker — both sides contend on one conditional update, so exactly one wins
|
|
|
616
643
|
and the loser is told. Treat a refusal as a normal outcome to surface, not
|
|
617
644
|
an error to retry.
|
|
618
645
|
|
|
619
|
-
`sql_where_clause` accepts a fragment against the dataset's
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
646
|
+
`sql_where_clause` accepts a fragment against the dataset's own
|
|
647
|
+
tables. For a **platform dataset** the server assigns the alias
|
|
648
|
+
(`le` legal entities, `m` messages, `al` action logs, `d` documents,
|
|
649
|
+
`bo` beneficial owners — same scheme as `data_activation_clients.md`
|
|
650
|
+
§6.5), and `id` is ambiguous across the base query's joins, so always
|
|
651
|
+
prefix it: `le.id`.
|
|
652
|
+
|
|
653
|
+
For a **generic-table** workflow there is no alias — the run selects
|
|
654
|
+
on the table's own columns directly, as the cookbooks do
|
|
655
|
+
(`customer_number IN (…)`). Prefixing one is an error, not a
|
|
656
|
+
precaution.
|
|
624
657
|
|
|
625
658
|
`manual_override: true` bypasses **dedupe/idempotency ONLY** —
|
|
626
659
|
the same row may fire again even though its idempotency tuple
|
|
@@ -647,9 +680,9 @@ context build + AI enrichment**, and the action's own
|
|
|
647
680
|
|
|
648
681
|
```typescript
|
|
649
682
|
const { data: result } = await api.workflows.execute(
|
|
650
|
-
tenantSlug, workflowSlug,
|
|
683
|
+
tenantSlug, datalakeSlug, workflowSlug,
|
|
651
684
|
{
|
|
652
|
-
dataset_id:
|
|
685
|
+
dataset_id: appointmentRowId, // the row's id — see below
|
|
653
686
|
decision_key: 'send_appointment_review_sms',
|
|
654
687
|
mode: 'live', // 'live' | 'dry_run' (same enum as .run)
|
|
655
688
|
manual_override: true, // load-bearing on re-fires
|
|
@@ -666,11 +699,22 @@ const { data: result } = await api.workflows.execute(
|
|
|
666
699
|
// result.scheduled_job_id — background job id (null when count 0)
|
|
667
700
|
```
|
|
668
701
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
702
|
+
`dataset_id` is simply the row's id, and **there is only one.** An
|
|
703
|
+
older version of this guide said to pass a tokenized-tier id and
|
|
704
|
+
warned that the raw one would not resolve. That was true of a design
|
|
705
|
+
with paired schemas and two rows per subject; GH-859 removed it.
|
|
706
|
+
Today the derived lakes are replicated copies that preserve the primary
|
|
707
|
+
key, so every lake reports the same id. And the lookup does not consult
|
|
708
|
+
a mode at all: `validate_dataset_exists/4` passes `:raw` as a **literal**
|
|
709
|
+
in both of its clauses — there is no branch, no mode argument threaded
|
|
710
|
+
in, and no third clause. Every dataset type resolves against the raw
|
|
711
|
+
lake.
|
|
712
|
+
|
|
713
|
+
So an id from a tokenized or redacted search is already the id
|
|
714
|
+
`.execute` wants. The re-search the old advice called for was not
|
|
715
|
+
merely unnecessary; it was answering a question the code never asks.
|
|
716
|
+
`primary-care` §029 reads one row through all three lakes on a single
|
|
717
|
+
slug, which is the direct evidence that the key survives replication.
|
|
674
718
|
|
|
675
719
|
`manual_override: true` on `.execute` sets the unique-key
|
|
676
720
|
constraint to `false` on the per-row job, allowing re-fires
|
|
@@ -745,16 +789,14 @@ Each log carries:
|
|
|
745
789
|
- `error_message` — coarse failure summary (e.g.
|
|
746
790
|
`"AI enrichment failed: <agent_name>"`); rich detail lives
|
|
747
791
|
in `error.json` (§ Per-step artifacts below)
|
|
748
|
-
- `action_execution_logs` — nested rows, one per action that fired
|
|
749
|
-
|
|
750
|
-
`dataAccessMode: 'regulated'` mode)
|
|
792
|
+
- `action_execution_logs` — nested rows, one per action that fired,
|
|
793
|
+
carrying the `message_body` virtual field
|
|
751
794
|
|
|
752
795
|
Methods:
|
|
753
796
|
|
|
754
797
|
```
|
|
755
798
|
.list(tenantSlug, datalakeSlug, workflowSlug, query?) Paged
|
|
756
|
-
.get(tenantSlug, datalakeSlug, workflowSlug, logId
|
|
757
|
-
{ dataAccessMode? }) One row
|
|
799
|
+
.get(tenantSlug, datalakeSlug, workflowSlug, logId) One row
|
|
758
800
|
.download(tenantSlug, datalakeSlug, workflowSlug, logId) The run's artifact
|
|
759
801
|
```
|
|
760
802
|
|
|
@@ -774,17 +816,20 @@ await api.workflows.workflowLogs.list(tenantSlug, datalakeSlug, slug, {
|
|
|
774
816
|
`filters` uses the Flop empty-bracket serialization on the wire —
|
|
775
817
|
the SDK's serializer handles it; never hand-roll the query string.
|
|
776
818
|
|
|
777
|
-
|
|
778
|
-
`
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
819
|
+
`.get` takes NO `dataAccessMode`. It used to; the platform removed the
|
|
820
|
+
`data_access_mode` query from this endpoint, and the generated type is now
|
|
821
|
+
`query?: never`, so passing one no longer compiles.
|
|
822
|
+
|
|
823
|
+
An execution log is workflow bookkeeping rather than lake rows, so there is
|
|
824
|
+
no tier to select. Choosing a tier belongs to the reads that return data —
|
|
825
|
+
`datasets.search` and `datalakes.executeSql` both take a mode, and that is
|
|
826
|
+
where `'raw' | 'tokenized' | 'redacted'` still applies.
|
|
782
827
|
|
|
783
828
|
### Per-step artifacts (event, filter, enrichment, error)
|
|
784
829
|
|
|
785
|
-
For each workflow execution, the platform writes JSON artifacts
|
|
786
|
-
|
|
787
|
-
|
|
830
|
+
For each workflow execution, the platform writes JSON artifacts to
|
|
831
|
+
the datalake's cloud-storage bucket. The keys follow a fixed
|
|
832
|
+
convention:
|
|
788
833
|
|
|
789
834
|
```
|
|
790
835
|
workflows/<workflow_id>/executions/<execution_log_id>/<step>.json
|
|
@@ -798,7 +843,7 @@ Fetch them via the datalake's `createDownloadLink`:
|
|
|
798
843
|
```typescript
|
|
799
844
|
const { data: link } = await api.datalakes.createDownloadLink(
|
|
800
845
|
tenantSlug, datalakeSlug,
|
|
801
|
-
{ bucket:
|
|
846
|
+
{ bucket: datalakeBucket, key: 'workflows/.../filter.json' },
|
|
802
847
|
)
|
|
803
848
|
const artifact = await (await fetch(link.url)).json()
|
|
804
849
|
```
|
|
@@ -984,15 +1029,16 @@ row doesn't swallow it. When counting "what actually went out", filter
|
|
|
984
1029
|
(Nested `workflow_ai_agents` follow the same whole-array-replace
|
|
985
1030
|
rule — re-supply the full set on every `update()`; see §3.)
|
|
986
1031
|
|
|
987
|
-
3. **`sql_where_clause` ids require an alias prefix
|
|
988
|
-
ambiguous across the multi-JOIN base query
|
|
989
|
-
`<base_alias>.id` (`
|
|
1032
|
+
3. **`sql_where_clause` ids require an alias prefix — on a platform
|
|
1033
|
+
dataset.** `id` is ambiguous across the multi-JOIN base query, so
|
|
1034
|
+
use `<base_alias>.id` (`le.id`, `m.id`). A generic-table workflow
|
|
1035
|
+
has no alias and takes bare column names.
|
|
990
1036
|
|
|
991
|
-
4. **`.execute`
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
calling `.execute`.
|
|
1037
|
+
4. **`.execute` and `.run` take the same id.** They once did not, and
|
|
1038
|
+
an older version of this guide described the asymmetry at length.
|
|
1039
|
+
The paired schemas that caused it are gone: the derived lakes
|
|
1040
|
+
preserve the primary key, so there is one id per row and no
|
|
1041
|
+
re-search is needed before calling `.execute`.
|
|
996
1042
|
|
|
997
1043
|
5. **`manual_override: true` on `.execute` is required for
|
|
998
1044
|
re-fires.** Without it, the per-row job dedupes against the
|
package/README.md
CHANGED
|
@@ -39,8 +39,8 @@ const api: PlatformApi = createIsolatedPlatformApi({
|
|
|
39
39
|
// 3. call resources — every nested resource is tenant + datalake scoped
|
|
40
40
|
const { data: datalake } = await api.datalakes.create(tenantSlug, {
|
|
41
41
|
name: 'Production Lake',
|
|
42
|
-
|
|
43
|
-
// …
|
|
42
|
+
type: 'raw',
|
|
43
|
+
// …db_reader_* / db_writer_* + cloud_storage credentials…
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
const { data, meta } = await api.tools.list(tenantSlug, datalake.slug)
|
|
@@ -28,53 +28,39 @@ function buildManagedBlock() {
|
|
|
28
28
|
"composing API calls for a scenario from primitives, start from the",
|
|
29
29
|
"closest recipe and diverge only with a stated reason.",
|
|
30
30
|
"",
|
|
31
|
-
"###
|
|
32
|
-
"",
|
|
33
|
-
"
|
|
34
|
-
"",
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"",
|
|
43
|
-
|
|
44
|
-
"",
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
`- \`${corpus}/cookbook/
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"",
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"",
|
|
65
|
-
"**Foundation**",
|
|
66
|
-
"",
|
|
67
|
-
`- \`${corpus}/cookbook/birthday-greeting-sms-trigger.md\` — send a`,
|
|
68
|
-
" happy-birthday SMS on each contact's next birthday using a",
|
|
69
|
-
" pure-Liquid trigger (year-roll math).",
|
|
70
|
-
`- \`${corpus}/cookbook/score-leads-with-llm-categorization.md\` — score`,
|
|
71
|
-
" inbound leads into four bands (hot / warm / cold / spam) via an LLM",
|
|
72
|
-
" agent, route each band to a tailored SMS action.",
|
|
73
|
-
`- \`${corpus}/cookbook/marketing-campaign-send.md\` — send an A/B`,
|
|
74
|
-
" marketing campaign across SMS and email: the suppression and",
|
|
75
|
-
" reachability gates and the A/B split all live in the workflow, each",
|
|
76
|
-
" send carries a per-recipient short link, and the reply re-attaches",
|
|
77
|
-
" to the customer who sent it.",
|
|
31
|
+
"### The four cookbooks",
|
|
32
|
+
"",
|
|
33
|
+
"One per use case. Each stands up its own tenant, datalake and",
|
|
34
|
+
"resources and shares nothing with the others — read one end to end",
|
|
35
|
+
"and you need no second file. Duplication between them is deliberate.",
|
|
36
|
+
"",
|
|
37
|
+
`- \`${corpus}/cookbook/organic-marketing.md\` — score inbound leads`,
|
|
38
|
+
" into four bands with an LLM agent, schedule a birthday greeting a",
|
|
39
|
+
" year out with a pure-Liquid trigger, and run an A/B campaign whose",
|
|
40
|
+
" suppression gates and split live in the workflow. Ends with",
|
|
41
|
+
" delivery reconciliation and a natural-language read of the lake.",
|
|
42
|
+
" One raw datalake.",
|
|
43
|
+
`- \`${corpus}/cookbook/payments-compliance.md\` — disambiguate`,
|
|
44
|
+
" gray-zone sanctions screenings with an LLM agent that never sees",
|
|
45
|
+
" who was screened, and fire a KYC notification on account",
|
|
46
|
+
" activation. Ends by reading one row through raw, tokenized and",
|
|
47
|
+
" redacted to show what each reader gets. Three datalakes.",
|
|
48
|
+
`- \`${corpus}/cookbook/subscription-saas.md\` — one customers table`,
|
|
49
|
+
" and three workflows over it: a welcome SMS gated on reachability,",
|
|
50
|
+
" a dunning reminder gated on reachability AND KYC, and an LLM agent",
|
|
51
|
+
" banding accounts by priority. Then the same table filled three",
|
|
52
|
+
" more ways — inline, bulk CSV, REST fetch — and a teammate invited",
|
|
53
|
+
" onto the tenant. One raw datalake.",
|
|
54
|
+
`- \`${corpus}/cookbook/primary-care.md\` — a review request that only`,
|
|
55
|
+
" reaches patients whose visit actually happened, an LLM agent",
|
|
56
|
+
" triaging inbound messages off the tokenized projection, and a",
|
|
57
|
+
" vision agent reading a scanned document. Three datalakes.",
|
|
58
|
+
"",
|
|
59
|
+
"**Capabilities are inside the cookbooks, not beside them.** Bulk",
|
|
60
|
+
"upload, REST fetch, team invitations, action status updaters,",
|
|
61
|
+
"paginated pollers, text-to-SQL and agent file-invocation each live in",
|
|
62
|
+
"the cookbook whose use case motivates them, as numbered steps. There",
|
|
63
|
+
"is no separate per-capability file to consult.",
|
|
78
64
|
"",
|
|
79
65
|
"### Top-3 consumer gotchas",
|
|
80
66
|
"",
|