Overview
- Modular SDK: The SDK now ships as an eager
velt.js core (bootstrap + facade) plus 19 lazy-loaded feature chunks (comment, cursor, presence, huddle, recorder, notification, reaction, and more). Each feature is fetched on demand instead of bundled into one eager payload.
- Modular SDK backward compatibility: The public
Velt / Snippyly browser API is unchanged. Existing browser integrations work as-is unless they also author Approval Engine definitions with the legacy rejection contract.
- New opt-in control APIs: Use
featureAllowList at init to preload only the features you use, and preload<Feature>() to warm a chunk or load tag-only features ahead of time.
Breaking Changes
-
[Modular SDK] None. The public
Velt / Snippyly API is unchanged. If you do not pass featureAllowList, the SDK preserves pre-modular behavior and preloads all chunks in the background.
-
[Approval Engine — unified
edges[] contract] The Approval Engine removed the per-human-node onReject shorthand and the authored top-level loops[] array. All workflow transitions — approve routing, reject routing, group fan-out/in, and loop-backs — are now expressed as a single unified edges[] array. Human nodes no longer carry any rejection config; a human node’s reject path is an outgoing on:"reject" edge.
- Who is affected: any definition that POSTs
HumanNodeConfig.onReject (routeToNodeId or loopBack) or a top-level loops[] array. These are now rejected at write time with INVALID_ARGUMENT.
- Who is not affected: already-stored definitions and in-flight executions. The runtime still consumes the compiled forward edges and derived loops unchanged.
- Why: the prior model had two ways to express rejection that desugared at write time into a form callers never saw, breaking round-trip symmetry. Under the unified model,
POST.edges === GET.edges.
How to Upgrade
Approval Engine: migrate to edges[]
Rewrite every onReject field and every loops[] entry as one or more edges[] entries before writing new definitions.
| Old form | New edge equivalent |
|---|
"onReject": { "routeToNodeId": "rework-notice" } | { "from": "human-review", "to": "rework-notice", "on": "reject" } |
"onReject": { "loopBack": { "toNodeId": "agent-draft", "maxIterations": 3 } } | { "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } } |
"loops": [{ "loopId": "...", "entryNodeId": "agent-draft", "bodyNodeIds": [...], "maxIterations": 3 }] | { "from": "human-review", "to": "agent-draft", "on": "reject", "loop": { "maxIterations": 3 } } |
onReject.loopBack.onExhausted.routeToNodeId | sibling { "from": "human-review", "to": "escalate", "on": "exhausted" } edge |
New EdgeSchema fields:
| Field | Type | Required | Notes |
|---|
from | EdgeEndpoint | yes | Bare node-id string, or { kind: "node", nodeId }, or { kind: "group", groupId }. |
to | EdgeEndpoint | yes | Same shape as from. |
on | "approve" | "reject" | "always" | "exhausted" | "custom" | no (default "always") | Semantic role. approve / reject auto-compile their predicates. |
when | JSON-AST string | only for on:"custom" | Custom predicate. Invalid on any other edge. |
loop | { maxIterations: 1–20 } | only on an on:"reject" back-edge | Marks the reject edge as a loop-back; the server derives the loop region. |
New INVALID_ARGUMENT compile error keys. A definition that violates the unified contract is rejected with one of these keys in the error message:
| Error key | Cause |
|---|
APPROVAL_EDGE_CUSTOM_REQUIRES_WHEN | on:"custom" without a non-empty when. |
APPROVAL_EDGE_WHEN_ONLY_FOR_CUSTOM | when supplied on a non-custom edge. |
APPROVAL_EDGE_LOOP_REQUIRES_REJECT | loop on a non-reject edge. |
APPROVAL_EDGE_LOOP_TARGET_NOT_ANCESTOR | reject+loop whose to is not an ancestor of from. |
APPROVAL_EDGE_REJECT_CYCLE_REQUIRES_LOOP | on:"reject" to an ancestor without loop (unmarked cycle). |
APPROVAL_EDGE_EXHAUSTED_REQUIRES_LOOP_SIBLING | on:"exhausted" with no sibling reject+loop from the same from. |
APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH | Human node with no outgoing on:"reject" edge. |
APPROVAL_EDGE_GROUP_TO_GROUP_FORBIDDEN | Both endpoints are group containers. |
APPROVAL_GROUP_FROM_REJECT_REQUIRES_LOOP | Forward on:"reject" from a joinOnQuorum / cancelOnQuorum group (dead edge — those policies fan out only on approval-quorum). |
APPROVAL_GROUP_FROM_LOOP_REQUIRES_JOINONQUORUM | reject+loop back-edge from a non-joinOnQuorum group. |
For the full edge model, group-as-edge-source semantics, and the read-only compiled block, see Customize Behavior and Create Definition.
How to Upgrade the Modular SDK
Adopting the modular SDK is optional. Update the package and your existing code keeps working. To opt into on-demand loading, use the following control APIs.
1. Preload only the features you use
Pass featureAllowList at init to preload only the listed chunks. Omit it to keep the default (all chunks preloaded in the background).
Valid modular feature keys are: 'comment', 'cursor', 'presence', 'huddle', 'recorder', 'notification', 'reaction', 'arrow', 'tag', 'rewriter', 'selection', 'area', 'activity', 'views', 'userInvite', 'userRequest', 'videoPlayer', 'crdt', and 'liveStateSync'.
React / Next.js
Other Frameworks
<VeltProvider apiKey={apiKey} initOptions={{ featureAllowList: ['comment', 'presence'] }}>
{children}
</VeltProvider>
Velt.init(apiKey, { featureAllowList: ['comment', 'presence'] });
2. Warm a chunk or load tag-only features with preload<Feature>()
Call a preloader to fetch a chunk ahead of use. This is also how you load tag-only features (userInvite, userRequest, videoPlayer) that have no element accessor. Preloaders are idempotent and non-throwing.
React / Next.js
Other Frameworks
client.preloadComment();
client.preloadUserInvite();
Velt.preloadComment();
Velt.preloadUserInvite();
3. getXElement() accessors auto-load their chunk
Every getXElement() accessor automatically loads its chunk and auto-extends the allow-list (ensureFeatureEnabled), so calling an accessor for a feature you didn’t list still works — you’re never locked out.
React / Next.js
Other Frameworks
// Auto-loads the comment chunk and enables the feature, even if not in featureAllowList
const commentElement = client.getCommentElement();
const commentElement = Velt.getCommentElement();
Tags placed before their chunk loads render inert and upgrade in place once the chunk lands — no remount or re-render required.