> ## Documentation Index
> Fetch the complete documentation index at: https://velt-mintlify-10684810.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Suggestions (Beta)

> Capture edits from humans or AI agents as reviewable suggestions with accept and reject actions on the comment dialog.

## 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`](/api-reference/sdk/models/data-models#suggestiont), and shows accept/reject buttons on the comment dialog.

<Note>
  The accept/reject UI renders on the Velt comment dialog, so your app needs Velt [Comments](/async-collaboration/comments/overview) set up. That's where reviewers act on a suggestion.
</Note>

## 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`](/api-reference/sdk/models/data-models#suggestiont) 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`](/api-reference/sdk/models/data-models#suggestiondata) 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](#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:

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useSuggestionUtils } from '@veltdev/react';

    const suggestionElement = useSuggestionUtils();
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    const suggestionElement = Velt.getSuggestionElement();
    ```
  </Tab>
</Tabs>

#### 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.

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    <input data-velt-suggestion-target="row.123.qty" type="number" defaultValue="5" />
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```html theme={null}
    <input data-velt-suggestion-target="row.123.qty" type="number" value="5">
    ```
  </Tab>
</Tabs>

**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:

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useRegisterTarget, useUnregisterTarget } from '@veltdev/react';
    import { useEffect } from 'react';

    function EditableRow() {
      const { registerTarget } = useRegisterTarget();
      const { unregisterTarget } = useUnregisterTarget();

      useEffect(() => {
        registerTarget({
          targetId: 'row.123',
          getter: () => ({
            qty: Number(document.getElementById('qty-input').value),
            price: Number(document.getElementById('price-input').value),
          }),
        });
        return () => unregisterTarget('row.123');
      }, []);

      return (
        <div data-velt-suggestion-target="row.123">
          <input id="qty-input" type="number" defaultValue="5" />
          <input id="price-input" type="number" defaultValue="99" />
        </div>
      );
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    suggestionElement.registerTarget({
      targetId: 'row.123',
      getter: () => ({
        qty: Number(document.getElementById('qty-input').value),
        price: Number(document.getElementById('price-input').value),
      }),
    });

    // Later, to remove the getter:
    suggestionElement.unregisterTarget('row.123');
    ```
  </Tab>
</Tabs>

<Warning>
  **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.
</Warning>

<Note>
  `registerTarget()` doesn't return an unsubscribe function. To remove a registration, call `unregisterTarget(targetId)`.
</Note>

#### 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`](/api-reference/sdk/models/data-models#enablesuggestionmodeconfig) to control how edits become suggestions (see [step 3](#3-capture-edits-as-suggestions)).

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useEnableSuggestionMode, useDisableSuggestionMode } from '@veltdev/react';

    function Toolbar() {
      const { enableSuggestionMode } = useEnableSuggestionMode();
      const { disableSuggestionMode } = useDisableSuggestionMode();

      return (
        <button onClick={() => enableSuggestionMode()}>Suggest changes</button>
      );
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    suggestionElement.enableSuggestionMode();

    // Later, return targets to normal editing:
    suggestionElement.disableSuggestionMode();
    ```
  </Tab>
</Tabs>

To show the current mode in your UI (for example, to highlight the "Suggesting" toggle), subscribe to it instead of reading it once:

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useSuggestionModeState } from '@veltdev/react';

    const isSuggesting = useSuggestionModeState(); // boolean, updates reactively
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    // Synchronous read
    const isSuggesting = suggestionElement.isSuggestionModeEnabled();

    // Reactive stream
    suggestionElement.isSuggestionModeEnabled$().subscribe((isEnabled) => {
      console.log('Suggestion mode:', isEnabled);
    });
    ```
  </Tab>
</Tabs>

#### 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.)

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    const { enableSuggestionMode } = useEnableSuggestionMode();

    enableSuggestionMode({
      onTargetEditStart: ({ targetId, oldValue }) => {
        // Informational: a user just started editing `targetId` (oldValue snapshotted).
      },
      onTargetEditCommit: ({ targetId, oldValue, newValue }) => {
        // Returning a result auto-creates the suggestion.
        return {
          summary: `${targetId}: ${oldValue} → ${newValue}`,
          metadata: { source: 'inline-edit' },
        };
      },
    });
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    suggestionElement.enableSuggestionMode({
      onTargetEditCommit: ({ targetId, oldValue, newValue }) => {
        return {
          summary: `${targetId}: ${oldValue} → ${newValue}`,
          metadata: { source: 'inline-edit' },
        };
      },
    });
    ```
  </Tab>
</Tabs>

<Tip>
  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.
</Tip>

**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.

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useSuggestionEventCallback } from '@veltdev/react';
    import { useEffect } from 'react';

    function CommitGate() {
      const commitEvent = useSuggestionEventCallback('targetEditCommit');

      useEffect(() => {
        if (!commitEvent) return;
        const { details, commitSuggestion } = commitEvent;
        if (isValid(details.newValue)) {
          commitSuggestion({ summary: `Update ${details.targetId}` });
        }
      }, [commitEvent]);

      return null;
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    suggestionElement.on('targetEditCommit').subscribe(({ details, commitSuggestion }) => {
      // `commitSuggestion` is a pre-bound builder. Call it to finalize the suggestion,
      // or skip it to discard this edit.
      if (isValid(details.newValue)) {
        commitSuggestion({ summary: `Update ${details.targetId}` });
      }
    });
    ```
  </Tab>
</Tabs>

**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.

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useStartSuggestion, useCommitSuggestion } from '@veltdev/react';

    function ProposeButton() {
      const { startSuggestion } = useStartSuggestion();
      const { commitSuggestion } = useCommitSuggestion();

      const propose = async () => {
        startSuggestion('row.123'); // snapshot oldValue now
        const { id } = await commitSuggestion({
          targetId: 'row.123',
          newValue: { qty: 7, price: 99 },
          summary: 'Bump qty + price',
          metadata: { source: 'manual' },
        });
        console.log('Created suggestion', id);
      };

      return <button onClick={propose}>Propose change</button>;
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    suggestionElement.startSuggestion('row.123');

    const { id } = await suggestionElement.commitSuggestion({
      targetId: 'row.123',
      newValue: { qty: 7, price: 99 },
      summary: 'Bump qty + price',
      metadata: { source: 'manual' },
    });
    ```
  </Tab>
</Tabs>

<Note>
  `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`.
</Note>

#### 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.

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useCommentEventCallback } from '@veltdev/react';
    import { useEffect } from 'react';

    function ApplyAcceptedSuggestions() {
      const accepted = useCommentEventCallback('suggestionAccepted');
      const rejected = useCommentEventCallback('suggestionRejected');

      useEffect(() => {
        const suggestion = accepted?.commentAnnotation?.suggestion;
        if (!suggestion) return;
        applyToYourState(suggestion.targetId, suggestion.newValue); // your code writes the change
      }, [accepted]);

      useEffect(() => {
        if (rejected?.commentAnnotation) {
          console.log('Rejected:', rejected.rejectReason);
        }
      }, [rejected]);

      return null;
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    const commentElement = Velt.getCommentElement();

    commentElement.on('suggestionAccepted').subscribe(({ commentAnnotation }) => {
      const suggestion = commentAnnotation?.suggestion; // suggestion.status === 'accepted'
      applyToYourState(suggestion.targetId, suggestion.newValue);
    });

    commentElement.on('suggestionRejected').subscribe(({ commentAnnotation, rejectReason }) => {
      console.log('Rejected:', rejectReason);
    });
    ```
  </Tab>
</Tabs>

<Note>
  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).
</Note>

<Warning>
  **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`.
</Warning>

#### 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`](/api-reference/sdk/models/data-models#suggestiongetsuggestionsfilter), or fetch the single pending suggestion for a target.

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    import { useSuggestions, usePendingSuggestion } from '@veltdev/react';

    // All suggestions, or filter by target / status
    const all = useSuggestions();
    const pendingForRow = useSuggestions({ targetId: 'row.123', status: 'pending' });

    // The newest pending suggestion for one target (or null)
    const pending = usePendingSuggestion('row.123');
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    // Synchronous snapshot
    const pending = suggestionElement.getSuggestions({ status: 'pending' });

    // Reactive stream
    suggestionElement.getSuggestions$({ targetId: 'row.123' }).subscribe((list) => {
      console.log('Suggestions for row.123:', list);
    });

    // Newest pending suggestion for a single target
    suggestionElement.getPendingSuggestion$('row.123').subscribe((s) => {
      console.log('Pending:', s);
    });
    ```
  </Tab>
</Tabs>

### 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](/api-reference/rest-apis/v2/comments-feature/comment-annotations/add-comment-annotations)
* See the [Agent Comments](/ai/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](/api-reference/rest-apis/v2/comments-feature/comment-annotations/get-comment-annotations-v2)

#### Update and Delete Suggestions

* Update annotation-level fields using the Update Comment Annotations REST API. [Learn more](/api-reference/rest-apis/v2/comments-feature/comment-annotations/update-comment-annotations)
* Delete suggestion threads using the Delete Comment Annotations REST API. [Learn more](/api-reference/rest-apis/v2/comments-feature/comment-annotations/delete-comment-annotations)

## Event Subscription

### [on](/api-reference/sdk/api/api-methods#on-6)

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.

| Category                          | Event Type           | Description                                                                  | Event Object                                                                           |
| --------------------------------- | -------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Review Outcomes (Comment Element) | `suggestionAccepted` | Triggered when a reviewer accepts a suggestion from the comment dialog       | [SuggestionAcceptEvent](/api-reference/sdk/models/data-models#suggestionacceptevent)   |
| Review Outcomes (Comment Element) | `suggestionRejected` | Triggered when a reviewer rejects a suggestion from the comment dialog       | [SuggestionRejectEvent](/api-reference/sdk/models/data-models#suggestionrejectevent)   |
| Lifecycle (SuggestionElement)     | `suggestionCreated`  | Triggered when a pending suggestion is created                               | [SuggestionCreatedEvent](/api-reference/sdk/models/data-models#suggestioncreatedevent) |
| Lifecycle (SuggestionElement)     | `suggestionStale`    | Triggered when a suggestion goes stale at accept time                        | [SuggestionStaleEvent](/api-reference/sdk/models/data-models#suggestionstaleevent)     |
| Editing (SuggestionElement)       | `targetEditStart`    | Triggered when a user focuses a target and editing begins                    | [TargetEditStartEvent](/api-reference/sdk/models/data-models#targeteditstartevent)     |
| Editing (SuggestionElement)       | `targetEditCommit`   | Triggered when an edit is committed. Carries the `commitSuggestion` builder. | [TargetEditCommitEvent](/api-reference/sdk/models/data-models#targeteditcommitevent)   |

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    // Hook: SuggestionElement events
    const suggestionEventCallbackData = useSuggestionEventCallback('suggestionCreated');
    useEffect(() => {
      if (suggestionEventCallbackData) {
        // Handle suggestion event response
      }
    }, [suggestionEventCallbackData]);

    // Hook: comment element events (accept / reject outcomes)
    const commentEventCallbackData = useCommentEventCallback('suggestionAccepted');
    useEffect(() => {
      if (commentEventCallbackData) {
        // Handle accept event response
      }
    }, [commentEventCallbackData]);

    // API Method
    const suggestionElement = client.getSuggestionElement();
    suggestionElement.on('suggestionCreated').subscribe((event) => {
      // Handle the event response
    });

    const commentElement = client.getCommentElement();
    commentElement.on('suggestionAccepted').subscribe((event) => {
      // Handle the event response
    });
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    const suggestionElement = Velt.getSuggestionElement();
    suggestionElement.on('suggestionCreated').subscribe((event) => {
      // Handle the event response
    });

    const commentElement = Velt.getCommentElement();
    commentElement.on('suggestionAccepted').subscribe((event) => {
      // Handle the event response
    });
    ```
  </Tab>
</Tabs>

## Lifecycle

A suggestion moves forward through these states (see [`SuggestionStatus`](/api-reference/sdk/models/data-models#suggestionstatus)):

| Status         | Meaning                                                                                               |
| -------------- | ----------------------------------------------------------------------------------------------------- |
| `pending`      | Created and awaiting review.                                                                          |
| `accepted`     | A reviewer accepted it; your `suggestionAccepted` handler applies `newValue`.                         |
| `rejected`     | A reviewer rejected it (optional `rejectReason`). Nothing is applied.                                 |
| `stale`        | The target DOM node couldn't be resolved at accept time, so the change can't be applied.              |
| `apply_failed` | Your accept handler threw while applying. Surfaced as a status only; there's no separate event in v1. |

## Data model

Suggestions are stored as [`CommentAnnotation`](/api-reference/sdk/models/data-models#commentannotation) objects with `type === 'suggestion'` and a populated [`suggestion`](/api-reference/sdk/models/data-models#suggestiondata) field. The full type hierarchy lives in the [`Suggestions`](/api-reference/sdk/models/data-models#suggestions) section of the Data Models reference.
