@curviate/sdk 0.43.0 → 0.44.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/CHANGELOG.md CHANGED
@@ -7,7 +7,73 @@ Versioning: semantic. Minor for additive changes, patch for bug fixes; no stabil
7
7
 
8
8
  ---
9
9
 
10
- ## [0.43.0] - Unreleased
10
+ ## [0.44.0] - 2026-10-22
11
+
12
+ Dashboard v2 API. Fixture and types regenerated from deployed staging (server
13
+ `15b704443878c7b589e40c7955a202b5131a20f7`, 197 paths), and the Sequence
14
+ corpus re-vendored from the same server.
15
+
16
+ ### Added
17
+
18
+ - **Contact `profile_picture_url`:** read-only, `string | null`. The person's
19
+ LinkedIn photo from profile data your team already holds; null when none is
20
+ held. The link is signed and expires.
21
+ - **Enrollments:** `enrollments.pause`, `enrollments.events` (send a custom
22
+ event to an `until event.<name>`), `enrollments.reassign` (move to another
23
+ sending account; `warnings` names Steps that will be skipped) and
24
+ `enrollments.bulk` (up to 1,000 by ids or filter, one result per
25
+ Enrollment, not atomic). `enrollments.resume` takes no body to lift your own
26
+ pause. `enrollments.retry(id)` retries a failed Step through the guarded
27
+ bulk path and throws `NOTHING_TO_RETRY` (or the Enrollment's other refusal)
28
+ as a `CurviateError`; `resume({ at })` is an explicit, unguarded jump to a
29
+ Step. New list filters:
30
+ `version`, `done_step` with `outcome`, `reason`, `variant_id`,
31
+ `relationship_at_enrollment`. Types `EnrollmentEventBody`,
32
+ `EnrollmentReassignBody`, `EnrollmentReassignResult`, `EnrollmentBulkBody`,
33
+ `EnrollmentBulkResult`, `EnrollmentBulkItem`, `EnrollmentReason` (the reasons catalogue).
34
+ - **Error codes** `SEQUENCE_CONFLICT` (with `err.current`: the stored
35
+ Sequence and its `sequence_revision`; send `if_sequence_revision` on
36
+ `motions.update`), `STEP_IN_FLIGHT`, `NOTHING_TO_RETRY`,
37
+ `ACCOUNT_ALREADY_ENGAGED`, `ALREADY_ENROLLED_ON_ACCOUNT`, `KNOWN_CHAT`,
38
+ `CHAT_NOT_ONE_TO_ONE`. Before, each decoded as `INTERNAL`.
39
+ - **Compiler-style Sequence errors:** every `validateSequence` entry, every
40
+ offline `SEQUENCE_INVALID` entry and every decoded wire `errors[]` entry of
41
+ a Sequence rule carries `code` (`R1` to `R24`, `W1` to `W13`), `message`,
42
+ `pointer` (a JSON Pointer into the document), `hint` and `docs_url`, beside
43
+ `path` and `rule`.
44
+ - **Sequence document:** short variables `{{first_name}}`,
45
+ `{{my_first_name}}` and `{{<key>}}` for a custom field (the long forms stay
46
+ valid); an optional `event` on `do` and `tag` steps; `until event.<name>`;
47
+ the `message.seen` predicate (`if` steps). Warnings `W11` (a short custom
48
+ field that looks like a typo of a standard field), `W12` (`message.seen`
49
+ before any message) and `W13` (a deprecated form).
50
+ - **`newStepId(document, kind, beforeId?)`:** the id the dashboard editor
51
+ gives a new step (`message`, `message_2`, `w_<step>`).
52
+ - **Webhooks:** `enrollment.accepted`, `enrollment.replied`, `enrollment.seen`,
53
+ `enrollment.reassigned` and `motion.custom_event` in `CurviateEvent`.
54
+ `account_ids` is optional (or `null`: every account you can operate) on
55
+ every source.
56
+
57
+ ### Changed
58
+
59
+ - **A/B variants are exactly 2** (`MAX_VARIANTS`). 3 to 5 are accepted with
60
+ warning `W13` until 2027-01-19, then refused (`R23`); 1, or 6 and more,
61
+ are refused as before, and 6 and more no longer compile.
62
+ - **`{{motion.<key>}}`** gives `W13` (Motion variables were removed) beside
63
+ `W9`; still not an error offline until 2027-01-19.
64
+
65
+ ### Deprecated
66
+
67
+ - `enrollments.signal` and `EnrollmentSignalBody`: use `enrollments.events`;
68
+ accepted until 2027-01-19.
69
+ - `until signal.<name>` (use `until event.<name>`) and the `motion.signal`
70
+ webhook event (renamed `motion.custom_event`), each accepted until
71
+ 2027-01-19.
72
+ - `emit` steps: legacy; put `event` on the `do` or `tag` step that matters.
73
+
74
+ ---
75
+
76
+ ## [0.43.0]
11
77
 
12
78
  Fixture and types regenerated from deployed staging (server
13
79
  `7359b1b1be0584f7f368f1367d39261f2de8f079`, 193 paths; earlier regens at
package/README.md CHANGED
@@ -143,26 +143,32 @@ const all = await curviate.contacts.export({ format: "json", tag: "vip" });
143
143
 
144
144
  ## Sequences
145
145
 
146
- A Motion runs one Sequence: a JSON document of steps (`do`, `wait`, `until`, `if`, `emit`, `tag`), run from the first step and ended explicitly with `"end"`. The SDK has an offline module for it: a typed builder that emits exactly that document, and `validateSequence`, which gives the verdict the API gives. Nothing in it touches the network, a key or the file system, so it runs in CI, browsers and edge functions. It needs TypeScript 5.0 or later.
146
+ A Motion runs one Sequence: a JSON document of steps (`do`, `wait`, `until`, `if`, `tag`, and the legacy `emit`), run from the first step and ended explicitly with `"end"`. The SDK has an offline module for it: a typed builder that emits exactly that document, and `validateSequence`, which gives the verdict the API gives. Nothing in it touches the network, a key or the file system, so it runs in CI, browsers and edge functions. It needs TypeScript 5.0 or later.
147
147
 
148
148
  ```ts
149
149
  import { Sequence, step } from "@curviate/sdk";
150
150
 
151
151
  const seq = Sequence.create({ on_reply: "end" }).add(
152
- step.do("invite", "invite.send", { with: { note: null, withdraw_after: "21d" } }),
152
+ step.do("invite", "invite.send", { with: { note: null, withdraw_after: "21d" }, event: "invite_sent" }),
153
153
  step.until("accepted", "invite.accepted", { timeout: "21d", then: "opener", else: "end" }),
154
154
  step.do("opener", "message.send", {
155
- with: { text: "Hi {{contact.first_name}}, thanks for connecting.", fallback: "Hi, thanks for connecting." },
156
- next: "end",
155
+ variants: [
156
+ { id: "named", text: "Hi {{first_name}}, {{opener}}" },
157
+ { id: "plain", text: "{{opener}}" },
158
+ ],
159
+ with: { fallback: "Hi, thanks for connecting." },
157
160
  }),
161
+ step.until("r_opener", "message.replied", { timeout: "4d", then: "end" }),
162
+ step.if("seen", { "message.seen": true }, { then: "bump", else: "end" }),
163
+ step.do("bump", "message.send", { with: { text: "{{followup}}", fallback: "Quick follow-up in case this got buried." }, next: "end" }),
158
164
  );
159
165
 
160
166
  const document = seq.toDocument(); // the Sequence document, keys in canonical order
161
167
  ```
162
168
 
163
- Field names are the document's keys (`on_reply`, `note_fallback`, `withdraw_after`). With literal arguments, the type checker refuses what the API would: a typo'd action, event, reaction or predicate, a `{{...}}` that is not a v1 variable (`{{firstName}}`, a `| default:` filter), a text with a variable and no fallback, a duration like `"1w"`, the reserved id `"end"`, and a jump to a step you never added (at `toDocument()`). Text the type checker cannot see (a `string` variable) is checked when you call `validate()` or `toDocument()`.
169
+ Field names are the document's keys (`on_reply`, `note_fallback`, `withdraw_after`). Variables: `{{first_name}}` (any standard Contact field), `{{my_first_name}}`, `{{my_last_name}}`, `{{my_full_name}}` (the sending account), and any other `{{<key>}}` is the custom field `contact.extra.<key>`; the long forms `{{contact.first_name}}`, `{{contact.extra.opener}}` and `{{account.first_name}}` stay valid. A messaging step takes one text in `with` or exactly 2 `variants` (A/B). An optional `event` on a `do` or `tag` step sends a custom event each time the step runs; `until event.<name>` waits for one your agent sends (`enrollments.events`). With literal arguments, the type checker refuses what the API would: a typo'd action, event, reaction or predicate, a `{{...}}` that is not a variable (`{{firstName}}`, `{{my_title}}`, a `| default:` filter), a text with a variable and no fallback, one variant, a duration like `"1w"`, the reserved id `"end"`, and a jump to a step you never added (at `toDocument()`). Text the type checker cannot see (a `string` variable) is checked when you call `validate()` or `toDocument()`.
164
170
 
165
- Every builder operation (`add`, `insertBefore`, `replace`, `remove`, `onReply`) returns a new builder. `toDocument()` throws `SEQUENCE_INVALID` with every error in `err.errors` as `{path, rule}`, the same list the API returns; warnings never block, read them with `validate()`. `JSON.stringify(seq)` equals `JSON.stringify(seq.toDocument())`.
171
+ Every builder operation (`add`, `insertBefore`, `replace`, `remove`, `onReply`) returns a new builder. `toDocument()` throws `SEQUENCE_INVALID` with every error in `err.errors`, the same list the API returns: `path`, `rule`, and the compiler-style `code` (`R1` to `R24`), `pointer` (a JSON Pointer into the document), `hint` (the fix) and `docs_url`. Warnings (`W1` to `W13`) never block, read them with `validate()`. `JSON.stringify(seq)` equals `JSON.stringify(seq.toDocument())`. `newStepId(document, kind)` names a new step as the dashboard editor does (`message`, `message_2`, `w_<step>` for a wait before a step).
166
172
 
167
173
  Raw JSON stays first class. Validate a document you wrote by hand, or load one (a Motion's `sequence`) to edit it:
168
174
 
@@ -170,14 +176,14 @@ Raw JSON stays first class. Validate a document you wrote by hand, or load one (
170
176
  import { Sequence, step, validateSequence } from "@curviate/sdk";
171
177
 
172
178
  const { errors, warnings } = validateSequence(JSON.parse(text));
173
- for (const e of errors) console.error(`${e.path}: ${e.rule}`);
179
+ for (const e of errors) console.error(`${e.path}: ${e.rule} (${e.code}: ${e.hint})`);
174
180
 
175
181
  const edited = Sequence.fromDocument(motion.sequence) // throws SEQUENCE_INVALID when invalid
176
182
  .insertBefore("opener", step.wait("settle", "1d"))
177
183
  .toDocument();
178
184
  ```
179
185
 
180
- `{{motion.<key>}}` is warning `W9` offline: only the API knows the Motion's `settings.variables`. Warnings that need the Motion's sending accounts or settings come from the API only. `SEQUENCE_SCHEMA_V1` is the published JSON Schema (`SEQUENCE_SCHEMA_URL`) for editors and tooling. The validator knows the vocabulary of the schema it ships with: a document using an action or event added to the API later is refused offline while the API accepts it; upgrade the SDK.
186
+ Deprecated forms are accepted with warning `W13` until 2027-01-19, then refused: 3 to 5 variants (use 2), `until signal.<name>` (use `until event.<name>`) and `{{motion.<key>}}` (write the text in place or use a custom field; offline it also gives `W9`). Warnings that need the Motion's sending accounts or settings come from the API only. `SEQUENCE_SCHEMA_V1` is the published JSON Schema (`SEQUENCE_SCHEMA_URL`) for editors and tooling. The validator knows the vocabulary of the schema it ships with: a document using an action or event added to the API later is refused offline while the API accepts it; upgrade the SDK.
181
187
 
182
188
  ## Motions and Enrollments
183
189
 
@@ -197,14 +203,22 @@ const motion = await curviate.motions.create({
197
203
  const { paths, samples, projection } = await curviate.motions.preview(motion.id); // a read: nothing is sent to LinkedIn
198
204
  await curviate.motions.start(motion.id);
199
205
 
200
- // Edit later: load the stored document into a builder.
206
+ // Edit later: load the stored document into a builder. if_sequence_revision refuses the save
207
+ // with SEQUENCE_CONFLICT (the stored document in err.current) when someone saved since.
201
208
  const current = await curviate.motions.get(motion.id);
202
209
  await curviate.motions.update(motion.id, {
203
210
  sequence: Sequence.fromDocument(current.sequence).insertBefore("opener", step.wait("settle", "1d")),
211
+ if_sequence_revision: current.sequence_revision,
204
212
  });
213
+
214
+ // Enrollments: one Contact's run on one account.
215
+ const { items: failed } = await curviate.enrollments.list({ motion_id: motion.id, done_step: "opener", outcome: "failed" });
216
+ await curviate.enrollments.bulk({ action: "retry", ids: failed.map((e) => e.id) }); // one result per Enrollment
217
+ await curviate.enrollments.retry("enr_123"); // one: throws NOTHING_TO_RETRY unless its Step failed
218
+ await curviate.enrollments.events("enr_123", { name: "research_ready" }); // moves an `until event.research_ready`
205
219
  ```
206
220
 
207
- A plain object goes as is and the API validates it; wrap hand-written JSON in `Sequence.fromDocument(json)` to have it checked offline first. `sequence: null` clears a draft's. Status operations: `start`, `pause`, `resume`, `archive` (irreversible), `delete` (drafts only); `addContacts` / `removeContacts` enroll or take out Contacts directly; `getSequenceVersion` reads a past version. `curviate.enrollments` lists one Contact's run per account (`list`, `get` with its Step history) and acts on it: `stop`, `resume({ at })`, `signal({ name })`. Refusals: `MOTION_NOT_STARTABLE` (every gap in `err.errors`), `MOTION_STATUS_CONFLICT`, `ENROLLMENT_NOT_ACTIVE`, `SEQUENCE_INVALID`.
221
+ A plain object goes as is and the API validates it; wrap hand-written JSON in `Sequence.fromDocument(json)` to have it checked offline first. `sequence: null` clears a draft's. Status operations: `start`, `pause`, `resume`, `archive` (irreversible), `delete` (drafts only); `addContacts` / `removeContacts` enroll or take out Contacts directly; `getSequenceVersion` reads a past version. `curviate.enrollments` lists one Contact's run per account (`list`, filtered by status, Step, Sequence version, or how a Step ended: `done_step` with `outcome`, `reason`, `variant_id`, `relationship_at_enrollment`; `get` with its Step history) and acts on it: `pause`, `resume()` (lift your pause), `retry(id)` (retry the failed Step; it acts only on a failure, else throws `NOTHING_TO_RETRY`, so a repeated call never sends a delivered message again), `resume({ at })` (an explicit jump to a Step, not guarded: it re-enters a Step that already sent; use `retry` for a failure), `events({ name })`, `reassign({ account_id })`, `stop`, and `bulk` for up to 1,000 at once by ids or filter. `signal` is the deprecated name of `events`, accepted until 2027-01-19. Refusals: `MOTION_NOT_STARTABLE` (every gap in `err.errors`), `MOTION_STATUS_CONFLICT`, `ENROLLMENT_NOT_ACTIVE`, `SEQUENCE_INVALID`, `SEQUENCE_CONFLICT`, `STEP_IN_FLIGHT` (retry in a minute), `NOTHING_TO_RETRY` (from `retry`, or per Enrollment in `bulk`), `ACCOUNT_ALREADY_ENGAGED`, `ALREADY_ENROLLED_ON_ACCOUNT`, `KNOWN_CHAT`.
208
222
 
209
223
  ## Network, Unibox and Analytics
210
224