Skip to main content

Overview

The Suggestions API adds suggestion mode to any input, editor, or custom component in your app. When it’s on, edits from a human (or an AI agent) aren’t written straight to your data. They’re captured as proposed changes that a reviewer accepts or rejects from the comment dialog, diff-style, like Google Docs suggestions. On accept, your app applies the change. You mark any DOM element as a suggestion target with a single attribute. The SDK captures the before and after values, saves each edit as a Suggestion, and shows accept/reject buttons on the comment dialog.
The accept/reject UI renders on the Velt comment dialog, so your app needs Velt Comments set up. That’s where reviewers act on a suggestion.

How it works

  1. You enable suggestion mode. The SDK starts watching every element tagged with data-velt-suggestion-target="<targetId>", including elements added to the DOM later.
  2. A user focuses a target. The SDK snapshots the target’s current value as the oldValue for that edit session.
  3. A user edits and commits the change (for example, by clicking away from a text field). The SDK reads the new value, compares it to the snapshot, and creates a pending Suggestion only if they differ.
  4. A reviewer accepts or rejects from the comment dialog. The outcome is emitted on the comment element as the suggestionAccepted / suggestionRejected events.
  5. Your app applies the change. Your suggestionAccepted handler reads commentAnnotation.suggestion.newValue and writes it into your own state or backend.

Properties

  • A suggestion is a regular comment annotation with type: "suggestion" and a populated suggestion field. There is no separate suggestions data store.
  • Suggestion mode is global for the current user and not persisted. A page reload returns to normal editing until you enable it again.
  • An edit is committed when the user finishes it, not on every keystroke. Text-like inputs (text, number, date, textarea, contenteditable) commit when the field loses focus (focusout), so each focus session produces at most one suggestion. Dropdowns, checkboxes, and radios commit on change, since picking a value is the whole edit.
  • Unchanged values never create suggestions. Focusing and blurring without editing is ignored, and commitSuggestion rejects a newValue that’s identical to the old value.
  • Events come from two elements: accept/reject outcomes are emitted on the comment element, while the rest of the lifecycle (suggestionCreated, suggestionStale, targetEditStart, targetEditCommit) is emitted on the SuggestionElement. See Event Subscription.
  • The SDK never mutates your data. It captures intent, orchestrates review, and persists the outcome. Applying the change is your code’s job.
  • Drift detection is best-effort. On accept, if a getter is registered, the live value is compared against oldValue; a mismatch sets driftDetected: true on the suggestion. v1 records the flag; a future release will surface a confirmation prompt.
  • Stale wins over drift. If the target DOM node can’t be resolved at accept time, the suggestion transitions to stale immediately and drift detection is skipped.

APIs

Frontend APIs

Follow these steps to add suggestions to your app. Steps 1 and 2 are the minimum to capture suggestions; step 3 controls how edits become suggestions, step 4 applies them once accepted, and step 5 lets you query suggestions for your own UI. All Suggestions methods live on a single SuggestionElement instance. Get it once and reuse it. In React, the hooks shown below wrap it for you, so you rarely need the element directly:

1. Define Suggestion Targets

Add the data-velt-suggestion-target="<targetId>" attribute to any element you want to track. The targetId is an ID you own and should stay stable, like the ID of the record the input edits. Don’t generate a random ID on each render: if the targetId changes, the SDK can’t match suggestions back to the element.
Do you need registerTarget? Usually not. For a single input, the SDK reads the value on its own: it checks for a registered getter first, then the form value (.value / .checked), then textContent. So a plain <input> needs no registerTarget call. You only need it when one target covers several inputs at once, like a table row with a qty field and a price field. There’s no single .value to read in that case, so you provide a getter function that returns the whole object:
Your getter must return what the user currently sees, not what’s saved. The SDK calls it twice: on focus to capture oldValue, and on commit to capture newValue. If it reads from app state that only updates after the user saves (common when suggestion mode is on), both calls return the same value and no suggestion is ever created. Read from the DOM (input.value), or for controlled inputs that update state on every keystroke, from that state.
registerTarget() doesn’t return an unsubscribe function. To remove a registration, call unregisterTarget(targetId).

2. Enable Suggestion Mode

Nothing is captured until you turn suggestion mode on, typically from a “Suggest changes” toggle in your toolbar. It applies to the whole page for the current user and resets on page reload, so enable it again after a refresh. Pass an optional EnableSuggestionModeConfig to control how edits become suggestions (see step 3).
To show the current mode in your UI (for example, to highlight the “Suggesting” toggle), subscribe to it instead of reading it once:

3. Capture Edits as Suggestions

When a user finishes an edit, you decide how it becomes a suggestion. There are three ways, from simplest to most control. Use one per target; only one of them handles any given edit. Option 1: Auto-commit with onTargetEditCommit (simplest) Pass onTargetEditCommit when you enable suggestion mode. The SDK calls it with the old and new values every time a user finishes an edit. Return an object and the SDK creates the suggestion right away, using your summary and metadata. This is the path most apps want. (onTargetEditStart fires when editing begins; it’s informational in v1 and its return value is reserved for future use.)
Don’t want every edit to become a suggestion automatically? Return null (or leave out onTargetEditCommit) and handle the edit yourself with the targetEditCommit event below, for example to validate the value or ask the user to confirm first.
Option 2: Decide per edit with the targetEditCommit event If you skip onTargetEditCommit, subscribe to the targetEditCommit event instead. The event gives you the edit details plus a commitSuggestion function that’s already tied to that edit. Call it to create the suggestion (you can override summary / metadata), or don’t call it to discard the edit. Nothing is created until you call it.
Option 3: Create suggestions manually with startSuggestion / commitSuggestion When there’s no input for the SDK to watch (a custom widget, a canvas element, or an “AI proposes a change” button), create the suggestion yourself. Call startSuggestion(targetId) to capture the current value as oldValue, then commitSuggestion(config) with the newValue to create it.
commitSuggestion only works while suggestion mode is on and the targetId is known to the SDK (tagged in the DOM or registered via registerTarget). It also creates nothing when newValue is identical to the captured oldValue.

4. Apply Accepted Suggestions

This is the step you can’t skip. When a reviewer clicks Accept or Reject on the comment dialog, the SDK updates the suggestion’s status but does not change your data. Listen for the suggestionAccepted event on the comment element (not the SuggestionElement), read commentAnnotation.suggestion.newValue, and write it to your state or backend yourself.
If the target element is no longer on the page when a reviewer accepts, the suggestion is marked stale instead of accepted. Listen for that on the SuggestionElement with useSuggestionEventCallback('suggestionStale') (React) or suggestionElement.on('suggestionStale') (other frameworks).
Your accept handler can run more than once (after reconnects, in multiple tabs, and on every client viewing the document), so applying newValue must be safe to repeat. Set the field to newValue rather than incrementing it. If your handler throws while applying, the SDK marks the suggestion apply_failed.

5. Get Suggestions

Beyond the built-in accept/reject buttons, you’ll often want to render your own indicators: a “1 pending change” badge on a row, a custom review panel, or a count in a toolbar. Query suggestions reactively with an optional SuggestionGetSuggestionsFilter, or fetch the single pending suggestion for a target.

Backend APIs

Suggestions are stored as comment annotations, so you manage them from your backend with the same Comment Annotations REST APIs.

Add Suggestions

  • Add agent-generated suggestions using the Add Comment Annotations REST API with type: "suggestion" and an agent block on the root comment. Learn more
  • See the Agent Comments guide for the full walkthrough of agent findings.

Get Suggestions

  • Get suggestion annotations using the Get Comment Annotations REST API. Use the agentSuggestions filter to return only fresh (unaccepted) agent suggestions. Learn more

Update and Delete Suggestions

  • Update annotation-level fields using the Update Comment Annotations REST API. Learn more
  • Delete suggestion threads using the Delete Comment Annotations REST API. Learn more

Event Subscription

on

Subscribe to Suggestion Events. Events come from two elements: review outcomes are emitted on the comment element, and the rest of the lifecycle is emitted on the SuggestionElement. Here is the list of events you can subscribe to and the event objects you will receive.

Lifecycle

A suggestion moves forward through these states (see SuggestionStatus):

Data model

Suggestions are stored as CommentAnnotation objects with type === 'suggestion' and a populated suggestion field. The full type hierarchy lives in the Suggestions section of the Data Models reference.