# CLAUDE Source: https://velt.dev/docs/CLAUDE # SKILLS SYNC PIPELINE Source: https://velt.dev/docs/SKILLS_SYNC_PIPELINE # Skills-Sync Pipeline — End-to-End Summary A multi-agent CI pipeline that watches `velt-js/docs`, detects when a merge to `main` should change rules in `velt-js/agent-skills`, edits the right files, validates the build, and opens (or updates) a single review-ready PR — automatically. Plus auto-propagation downstream into `velt-plugin-cursor` and `velt-plugin-claude`. *** ## How the pipeline works ### Trigger Every push to `velt-js/docs/main` touching one of the watched paths runs `.github/workflows/docs-sync-skills.yml`: ``` async-collaboration/** realtime-collaboration/** api-reference/rest-apis/** self-host-data/** security/** webhooks/** get-started/** backend-sdks/** ui-customization/** permission-management/** in-app-user-feedback/** ``` ### Three jobs **Job 1 — `plan`** (Stage 0 script + Agent 1, Opus 4.7) * `compute-diff.sh` produces a structured diff of the docs change. * `Agent-1-docs-diff-planner` reads `mapping.md` and emits `plan.json`: * `tickets[]` — one entry per impacted skill (`{skill, paths, intent, changes}`) * `unmapped[]` — feature paths with no current skill (issues filed, no auto-create) **Job 2 — `update-skill`** (matrix, one job per ticket; Agent 2 variant, Opus 4.7) `dispatch-updater-variant.sh` maps each skill to one of six variants: | Variant | Skills | | -------------------------- | --------------------------------------------------- | | **2a** `frontend-async` | comments, notifications, activity, recorder | | **2b** `frontend-realtime` | presence, cursors, huddle, single-editor-mode, crdt | | **2c** `setup` | velt-setup-best-practices | | **2d** `backend` | rest-apis, self-hosting-data | | **2e** `infra` | proxy-server, yjs | | **2f** `ai` | rewriter (extensible for future AI features) | The chosen variant edits **source rule files only** (never AGENTS files), bumps `metadata.json` version, optionally adds a new category to `_sections.md`. **Job 3 — `validate-and-pr`** (Agent 3 Haiku 4.5, Agent 4 Opus 4.7) * **Resolve branch:** queries `gh pr list --label skills-sync --state open`. If a PR is open, checks out its branch and exports `EXISTING_PR_NUMBER`. Otherwise creates `skills-sync/`. * `apply-patches.sh` merges all matrix artifacts onto the branch. * `Agent-3-skill-format-validator` runs `npm run validate && npm run build` to regenerate `AGENTS.md` / `AGENTS.full.md`. * `Agent-4-skill-pr-composer` either updates the existing PR (`gh pr edit` body, `--add-reviewer yoen-velt`, `gh pr comment` for timeline visibility) or opens a new one (`--reviewer yoen-velt`, labels `skills-sync` + `automated`). ### Sync downstream to plugins (automatic, separate workflow) The merge to `agent-skills/main` triggers `agent-skills/.github/workflows/sync-to-plugins.yml`, which copies `skills/**` into `velt-plugin-cursor` and `velt-plugin-claude` and opens sync PRs there. ### Weekly drift check (independent, no PRs) `docs/.github/workflows/skills-sync-drift-check.yml` runs every Monday at 14:00 UTC, comparing the docs feature tree to the agent-skills inventory and filing (or updating) a rollup issue in agent-skills if anything has drifted out of sync. ### Guardrails on the receiving end `agent-skills/.github/workflows/skills-sync-guardrails.yml` fires on every PR labeled `skills-sync` and rejects: * Edits inside `*-workspace/**` or `**/evals/**`. * Rewrites of `metadata.json.abstract` or `.author` without the `manual-skill-rewrite` label. * `metadata.json.version` not bumping when rule files change (unless labeled `docs-prose-only`). * `AGENTS.md` changes without a sibling rule change. *** ## Visual ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ velt-js/docs (you merge here) │ │ │ │ PR → main, touches any of: async-collaboration/**, realtime/**, │ │ ui-customization/**, get-started/**, security/**, etc. │ └──────────────────────────────┬──────────────────────────────────────────────┘ │ push trigger ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ .github/workflows/docs-sync-skills.yml │ │ │ │ ┌──────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │ │ │ plan │───▶│ update-skill │───▶│ validate-and-pr │ │ │ │ (Agent 1)│ │ (Agent 2 a..f) │ │ (Agent 3 + Agent 4) │ │ │ │ Opus │ │ Opus, matrix │ │ Haiku + Opus │ │ │ └──────────┘ └──────────────────┘ └────────────┬─────────────┘ │ │ plan.json per-ticket patches build + PR │ │ │ │ │ └───────────────────────────────────────────────────────┼─────────────────────┘ │ ┌────────────────────────┴───────────────┐ │ Existing open `skills-sync` PR? │ │ YES → push commit + `gh pr edit` │ │ (cumulative body) │ │ NO → `gh pr create --reviewer │ │ yoen-velt --label skills-sync` │ └────────────────────┬───────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ velt-js/agent-skills (skills-sync PR opens here) │ │ │ │ PR labeled `skills-sync`, `automated`, reviewer: @yoen-velt │ │ Body: per-skill summary, source-commit link, pipeline-run link │ │ │ │ On PR open, secondary CI fires: │ │ .github/workflows/skills-sync-guardrails.yml │ │ • blocks edits in *-workspace/** or **/evals/** │ │ • requires metadata.json version bump on rule changes │ │ • blocks AGENTS.md changes without sibling rule change │ │ │ │ Reviewer merges to agent-skills/main │ └─────────────────────────────────────────────────┬───────────────────────────┘ │ push to agent-skills/main ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ velt-js/agent-skills/.github/workflows/sync-to-plugins.yml │ │ │ │ Triggered automatically on every push to agent-skills/main touching │ │ skills/**. Copies skill files into the two plugin repos: │ └──────────┬───────────────────────────────────────────────┬──────────────────┘ │ │ ▼ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ velt-js/velt-plugin-cursor │ │ velt-js/velt-plugin-claude │ │ (auto sync PR opens) │ │ (auto sync PR opens) │ └──────────────────────────────┘ └──────────────────────────────┘ ──────────────────────────────────────────────────────────────────────────────── Independent: weekly drift check ──────────────────────────────────────────────────────────────────────────────── ┌─────────────────────────────────────────────────────────────────────────────┐ │ Monday 14:00 UTC │ │ docs/.github/workflows/skills-sync-drift-check.yml │ │ │ │ Compares docs feature tree ↔ agent-skills/skills/ inventory │ │ Drift found? → files (or updates) rollup issue in agent-skills │ │ No drift? → silent (no PR, no issue) │ └─────────────────────────────────────────────────────────────────────────────┘ ``` *** ## File map ### Pipeline definition (in `velt-js/docs`) | File | Purpose | | -------------------------------------------------------------- | --------------------------------------------- | | `.github/workflows/docs-sync-skills.yml` | Main pipeline trigger + 3-job orchestration. | | `.github/workflows/skills-sync-drift-check.yml` | Weekly Monday drift audit. | | `.claude/agents/skills-sync/Agent-1-docs-diff-planner.md` | Plans which skills to touch. (Opus 4.7) | | `.claude/agents/skills-sync/Agent-2a..f-*-skill-updater.md` | One variant per skill family. (Opus 4.7) | | `.claude/agents/skills-sync/Agent-3-skill-format-validator.md` | `npm run validate && build`. (Haiku 4.5) | | `.claude/agents/skills-sync/Agent-4-skill-pr-composer.md` | Opens / updates the PR. (Opus 4.7) | | `scripts/skills-sync/*.sh` | Plumbing — diff, dispatch, apply, smoke-test. | | `scripts/skills-sync/reference-artifacts/mapping.md` | Frozen docs-path → skill mapping. | | `scripts/skills-sync/reference-artifacts/format-guide.md` | Authoritative skill-file format. | ### Receiving end (in `velt-js/agent-skills`) | File | Purpose | | ---------------------------------------------------------------------- | ------------------------------------------------------------------- | | `.github/workflows/skills-sync-guardrails.yml` | PR-level guardrails (rule-bump, workspace-edit blocks). | | `.github/workflows/sync-to-plugins.yml` | Propagates merged changes to both plugin repos. | | `.github/CODEOWNERS` | Ownership documentation only. **Not** the review-routing mechanism. | | `skills//SKILL.md`, `metadata.json`, `rules/shared/_sections.md` | Source-of-truth files updater agents edit. | | `skills//AGENTS.md`, `AGENTS.full.md` | Build artifacts. **Never hand-edited.** | ### Required secrets (in `velt-js/docs`) | Secret | Used for | | ------------------- | ------------------------------------------------------------------------- | | `ANTHROPIC_API_KEY` | All four Claude agents. | | `AGENT_SKILLS_PAT` | Cross-repo writes (checkout, push, PR, issues) on `velt-js/agent-skills`. | *** ## How we set it up (chronological steps) 1. **Tested the pipeline on a no-op commit** (`44ae9b9` "Added proxy server to docs") — Plugin-Agent-1 correctly returned `hasDeltas: false` because the agent-skills repo already reflected that change. 2. **Tested the pipeline on a real release** (`650d917e` "v5.0.2-beta.18") — Plugin-Agent-1 produced 3 Comments deltas and parked 2 Rewriter deltas (no library existed yet). 3. **Manually scaffolded `velt-rewriter-best-practices`** in `velt-js/agent-skills` (per the policy that the pipeline never auto-creates new skills — it files an issue, humans decide). 4. **Plugin-Agent-2 patched** the 3 Comments rule files (`mode-inline-comments.md`, `ui-wireframes.md`, `config-ui-behavior.md`) and the 2 new Rewriter rule files (`api-default-ui-toggle.md`, `types-ai-model.md`). 5. **Caught and fixed a fabricated component name** in `ui-wireframes.md` — Plugin-Agent-1 had inferred `VeltInlineCommentsSectionWireframe.ShowMore` from prose; the real components are `VeltCommentDialogWireframe.ThreadCard.Message.ShowMore` / `.ShowLess`. 6. **Diagnosed two build bugs** surfaced by `npm run build`: proxy-server's `metadata.json` used a non-canonical schema (`{name, author, ruleCount, categories}`) so the build emitted `undefined|undefined`; self-hosting-data's `_sections.md` was missing entries for `data/` and `python-sdk/` so 5 rule files were orphaned. 7. **Migrated proxy-server's `metadata.json`** to the canonical schema (`{version, organization, date, abstract, references}`) used by every other library. 8. **Reformatted proxy-server's `_sections.md`** from the flat-list format to the canonical `## N. Title (folder)` + `**Impact:**` shape the parser expects. 9. **Reconnected self-hosting-data's orphaned folders** by adding Sections 6 (Data Types) and 7 (Python SDK) to its `_sections.md` (Debugging renumbered to 8). 10. **Created `_sections.md` for the new Rewriter library** defining `api/` and `types/` sections. 11. **Created a new Agent-2f variant** (`Agent-2f-ai-skill-updater.md`) for AI/productivity features, modeled on the infra variant; updated `mapping.md` and `dispatch-updater-variant.sh` so rewriter routes to it. 12. **Added `ai/rewriter/**` → `velt-rewriter-best-practices`** routing to `mapping.md` so future docs changes in that path land on the right library. 13. **Retired the old `docs-sync.yml`** workflow (it was a single-job shell pipeline that opened PRs in 3 repos on every `**/*.mdx` change) — superseded by the new agent-driven `docs-sync-skills.yml`. 14. **Wired Agent 4 to reuse open PRs** — if a `skills-sync` PR is already open, the next pipeline run commits onto its branch and `gh pr edit`s the body, rather than opening a parallel PR. 15. **Changed review routing from CODEOWNERS to direct `@yoen-velt`** — Agent 4 now passes `--reviewer yoen-velt` on PR creation and `--add-reviewer yoen-velt` on PR edit; CODEOWNERS file is kept for ownership documentation only. 16. **Added the Rewriter row to `agent-skills/.github/CODEOWNERS`** in a new "AI / productivity" section for completeness. 17. **Reconciled rule counts** to match build output — Comments 71→69, Recorder 21→23, Self-Hosting-Data 13→21 (+8 from reconnected sections), Rewriter +2 new, Total 277→282. 18. **Regenerated all 10 buildable libraries' `AGENTS.md` / `AGENTS.full.md`** files via `npm run build` (Comments now correctly indexes the new beta.18 content; proxy-server is no longer broken). 19. **Updated customer-facing docs** — added a Rewriter Card to `docs/get-started/skills.mdx`, added a use-case bullet and mapping row, updated counts in `docs/get-started/plugins.mdx` for both plugin trees. 20. **Confirmed `ANTHROPIC_API_KEY` already exists as a GitHub Actions secret** on `velt-js/docs` (rotated since it was exposed in chat). 21. **Committed 3 commits to agent-skills/main** (`Workflow fix`, `v5.0.2-beta.18 ...`, `Build fix + AGENTS regen ...`) and pushed. 22. **Committed 2 commits to a docs feature branch** (`Add docs-sync-skills pipeline ...`, `v5.0.2-beta.18 docs ...`). 23. **Cherry-picked just those 2 commits onto `docs/main` and pushed** — pipeline is now live. Other wireframe-variables work stayed on the feature branch for normal PR review. *** ## Carryover / out-of-scope workstreams These exist but are intentionally not part of this auto-updater work: 1. **5 libraries still lack `metadata.json`** in the canonical schema: presence, cursors, huddle, rest-apis, yjs. They aren't picked up by the build and their `AGENTS.md` files remain stale. The customer-facing rule counts for those (presence 13, cursors 11, huddle 10, rest-apis 9) are inherited and unverified. 2. **The `@velt-js/ai-owners` team handle in `CODEOWNERS`** is a placeholder. Since review routing is direct to `@yoen-velt`, this is harmless — but worth resolving when CODEOWNERS becomes the routing mechanism for any other workflow. 3. **Wireframe-variables work** (\~30 pages of new per-feature wireframe-variables docs) is still sitting on the feature branch `wireframe-variables-non-comments-notifications` — 8 commits ahead of main. Not pushed to `main` because that's normal-PR-review territory. *** ## Verifying the pipeline works Next time you merge a docs PR to `main` that touches a watched path: 1. **Actions tab → docs-sync-skills**: watch the three jobs (`plan`, `update-skill`, `validate-and-pr`) run end-to-end. Should finish in \~5–10 min. 2. **agent-skills → Pull Requests**: filter `is:pr is:open label:skills-sync`. Expect either a new PR or a new commit on an existing one, with `@yoen-velt` requested. 3. **Worst-case diagnostic**: download the workflow run's `outputs/pr-composer-result.json` artifact for the status code and any error message. # WIREFRAME VARIABLES TEMPLATE Source: https://velt.dev/docs/WIREFRAME_VARIABLES_TEMPLATE # Wireframe Variables — Documentation Template This is the authoring guide for **per-feature wireframe variable pages** in the public docs. Every feature in `sdk/docs/wireframe-variables/` should eventually have a matching MDX page in `ui-customization/features/...`. Use this template verbatim — consistency across features is the point. > **Scope reminder.** "Wireframe variables" here means the dynamic template tokens (e.g. `{user.name}`, `{annotation.status}`) that are read by `velt-data` / `velt-if` / `velt-class` inside a ``. They are **not** CSS variables. CSS variables are documented separately under `ui-customization/styling.mdx` and feature-level `variables.mdx` pages. *** ## 1. When to use this template * Trigger: a feature has a source reference at `sdk/docs/wireframe-variables/.md` but no public-docs counterpart. * Output: one new MDX page per source file, placed inside the existing feature folder alongside the existing `wireframes` and `primitives` files. * Title (in frontmatter): always ` Wireframe Variables`, where `` matches the feature's `group` label in `docs.json` (e.g. `Activity Logs`, `Comment Dialog`, `Notifications Panel`). ### Filename convention — match the siblings Filenames in the docs repo are inconsistent across feature folders. **Match the convention used by the sibling `wireframes` / `primitives` files in the same folder.** | Sibling files look like… | Use this filename | | ----------------------------------------------------------------- | ----------------------------------- | | `wireframes.mdx`, `primitives.mdx` (bare) | `wireframe-variables.mdx` | | `-wireframes.mdx`, `-primitives.mdx` (prefixed) | `-wireframe-variables.mdx` | Examples: ``` ui-customization/features/async/comments/comment-dialog/ ├── wireframes.mdx ├── primitives.mdx └── wireframe-variables.mdx ← bare convention ui-customization/features/async/activity-logs/ ├── activity-logs-wireframes.mdx ├── activity-logs-primitives.mdx └── activity-logs-wireframe-variables.mdx ← prefixed convention ``` ### Source → docs map | Public docs page (to author) | Source file | | -------------------------------------------------------------------------- | ----------------------------------------------------- | | `ui-customization/.../activity-logs/activity-logs-wireframe-variables.mdx` | `sdk/docs/wireframe-variables/activity-log.md` | | `ui-customization/.../comment-dialog/wireframe-variables.mdx` | `sdk/docs/wireframe-variables/comment-dialog.md` | | `ui-customization/.../comment-sidebar/wireframe-variables.mdx` | `sdk/docs/wireframe-variables/sidebar.md` | | `ui-customization/.../notifications/panel/wireframe-variables.mdx` | `sdk/docs/wireframe-variables/notifications-panel.md` | | ...one per file in `sdk/docs/wireframe-variables/` | (25 source files total) | *** ## 2. Mapped vs flat-config — pick one mode The source `README.md` defines two access patterns. **Pick the one that matches the feature** and follow only that branch of the skeleton below. | Pattern | Features | Variable form in tables | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | **Mapped** | Comment Dialog, Comments Sidebar, Comment Bubble, Inline Comments Section, Multi-Thread Comment Dialog, Text Comment, Notifications Panel, Notifications Tool, Activity Log | Bare names, e.g. `user`, `annotation`, `darkMode` | | **Flat-config** | Cursor, Presence, Huddle, Recording, Transcription, View Analytics, Area, Arrow, Tag, Reactions, Selection, Rewriter, Comments Tool, Sidebar Button, Autocomplete | Explicit prefix, e.g. `componentConfig.user`, `componentConfig.cursorUsers` | * **Mapped** features → keep the four state buckets (`App State` / `Data State` / `UI State` / `Feature State`). Drop any bucket the source has nothing for. If the source MD has a separate "Per-instance Local UI State" bucket, fold it into `UI State` — do **not** add a fifth bucket. * **Flat-config** features → replace the four buckets with a single `## Component Config` section, and keep the "flat-config" `` callout in the skeleton. *** ## 3. Page sections — required order Every page follows this exact order. Sections in **(optional)** are included only when the source MD has the corresponding content. 1. Frontmatter 2. Intro `` linking to overview + template variables 3. `## Overview` (3-form usage table + mapped/flat-config note) 4. `## App State` / `## Data State` / `## UI State` / `## Feature State` (mapped) **or** `## Component Config` (flat-config) 5. `## Common Props` *(optional — include only if the feature documents shared attributes / signal inputs)* 6. `## Context-Specific Variables` *(optional — only when the source has it; include the alias `` directly under the table when the source mentions aliases)* 7. `## Type Reference` *(optional — slim summary table linking out to `data-models`; include when the page references 2+ compound types)* 8. `## Subcomponents` *(optional — one `### ` per item that has at least one of: extra variables, a `shouldShow` condition, a host class, or a meaningful example)* 9. `## Deeply-Nested Wireframe Tags` *(optional — flattens the long tail of leaf tags the source documents at the bottom)* 10. `## Putting it together` (always — exactly one realistic combined example, in ``) 11. `## Related` (always — links to siblings + template-variables overview) *** ## 4. Page skeleton (copy-paste scaffold) Replace every ``, every example variable, and the `` with real content from the source file. Delete sections that don't apply. ````mdx theme={null} --- title: Wireframe Variables description: Template variables exposed by the wireframe — read them with velt-data, velt-if, and velt-class to drive dynamic content, conditional rendering, and class toggling. --- New to wireframes? Start with [UI Customization Concepts](/ui-customization/overview) and the [Template Variables](/ui-customization/template-variables) overview. ## Overview The **** wireframe exposes the variables below. Use them inside any `` tag via three forms: | You want to… | Use | Example | |---|---|---| | Display a value as text | `` | `` | | Hide / show conditionally | `velt-if="{var}"` | `velt-if="{enableResolve}"` | | Toggle a CSS class | `velt-class="'cls': {var}"` | `velt-class="'is-dark': {darkMode}"` | {/* MAPPED FEATURES — include this line. */} All variables are mapped — reference them by their short name. You do not need the `componentConfig.` prefix. {/* FLAT-CONFIG FEATURES — replace the line above with this Note. */} This feature uses the **flat-config** access pattern — variables must be referenced with the explicit `componentConfig.` prefix, e.g. `field="componentConfig.user.name"`. {/* ───────────────────────────────────────────────────────────────── MAPPED FEATURES — use the four buckets below. Drop any the source has nothing for. Fold any "Per-instance Local UI State" rows into UI State. ───────────────────────────────────────────────────────────────── */} ## App State App-wide values resolved from the shared signal. | Variable | Type | Description | Example | |---|---|---|---| | `user` | [`User`](/api-reference/sdk/models/data-models#user) | Currently identified end-user. | `` | | `darkMode` | `boolean` | Dark mode active (global config or per-instance attribute). | `velt-class="'theme-dark': {darkMode}"` | ## Data State Per-feature data: the live records, lists, and selection / filter state. | Variable | Type | Description | Example | |---|---|---|---| | `annotation` | [`CommentAnnotation`](/api-reference/sdk/models/data-models#commentannotation) | Annotation rendered by this wireframe instance. | `` | | `editComment` | [`Comment`](/api-reference/sdk/models/data-models#comment) \| `null` | Comment currently being edited, if any. | `velt-if="{editComment}"` | ## UI State Per-instance UI flags driven by the component itself. | Variable | Type | Description | Example | |---|---|---|---| | `isOpen` | `boolean` | Panel is open. | `velt-class="'is-open': {isOpen}"` | | `composerInOpenState` | `boolean` | Composer is currently expanded. | `velt-if="{composerInOpenState}"` | | `variant` | `string` | Per-instance variant tag set on the host element. | `` | ## Feature State Capability flags toggled via SDK config. | Variable | Type | Description | Example | |---|---|---|---| | `isEnabled` | `boolean` | Feature is enabled in the SDK. | `velt-if="{isEnabled}"` | | `enableResolve` | `boolean` | Resolve action enabled by config. | `velt-if="{enableResolve}"` | {/* ───────────────────────────────────────────────────────────────── FLAT-CONFIG FEATURES — delete the four buckets above and use this single section instead. ───────────────────────────────────────────────────────────────── ## Component Config | Variable | Type | Description | Example | |---|---|---|---| | `componentConfig.user` | [`User`](/api-reference/sdk/models/data-models#user) | Currently identified end-user. | `` | | `componentConfig.cursorUsers` | [`User`](/api-reference/sdk/models/data-models#user)`[]` | Active remote cursor users. | `` | ───────────────────────────────────────────────────────────────── */} {/* OPTIONAL — Common Props. Include if the source documents shared attributes that every primitive in this feature accepts. */} ## Common Props Every primitive accepts: | React Prop | HTML Attribute | Type | Default | Description | |---|---|---|---|---| | `defaultCondition` | `default-condition` | `boolean \| "true" \| "false"` | `true` | When `false`, the component always renders regardless of internal state. | Signal inputs (for parent-child component composition): - `[componentConfigSignal]` — shared config signal. - `[parentLocalUIState]` — per-instance UI state signal. The root `` element additionally accepts attributes that map onto config and local UI state values: `dark-mode`, `variant`, `is-open`, etc. {/* OPTIONAL — Context-Specific Variables. Only include if the source has variables only resolvable inside specific nested wireframe tags. Include the alias only when the source mentions aliases. */} ## Context-Specific Variables These are only resolvable inside the nested wireframe tags noted in the **Available in** column. Each is injected by the iteration primitive that owns the tag. | Variable | Type | Available in | Example | |---|---|---|---| | `commentObj` | [`Comment`](/api-reference/sdk/models/data-models#comment) | `` and descendants | `` | | `commentIndex` | `number` | Same as above. `0` on the parent comment. | `velt-if="{commentIndex} === 0"` | **Aliases:** `commentObj` ↔ `comment`. Either name resolves; the friendly short form is preferred. {/* OPTIONAL — Type Reference. A SLIM summary table linking to data-models. Do NOT inline full type field tables. Include only when 2+ compound types are referenced on the page. */} ## Type Reference Types referenced by the variables above are documented in [Data Models](/api-reference/sdk/models/data-models): | Type | Description | |---|---| | [`CommentAnnotation`](/api-reference/sdk/models/data-models#commentannotation) | The annotation thread (id, status, comments, etc.). | | [`Comment`](/api-reference/sdk/models/data-models#comment) | A single message inside an annotation thread. | | [`User`](/api-reference/sdk/models/data-models#user) | Identified end-user object. | {/* OPTIONAL — Subcomponents. One `### ` per subcomponent that has at least one of: extra variables, a shouldShow condition, a host class, or a meaningful example. Use the Property/Value table for metadata. Use a 3-col Variable | Type | Example table (NO Description column) for extra variables. */} ## Subcomponents Each subcomponent below has its own wireframe tag. The tables show extra variables beyond the common set, the `shouldShow` condition, and host CSS classes. ### `` (root) - **Public element:** `>` - **Wireframe tag:** `-wireframe>` | Property | Value | |---|---| | Extra variables | None — root only sees common variables. | | `shouldShow` | `isEnabled === true && isOpen === true` | | Host class | `velt---container` (always present) | ```jsx Wireframe velt-if="{isEnabled} && {isOpen}"> Wireframe.Header /> Wireframe.Body /> Wireframe> ``` ```html -wireframe velt-if="{isEnabled} && {isOpen}"> -header-wireframe>-header-wireframe> -body-wireframe>-body-wireframe> -wireframe> ``` --- ### `-list-item` A single row in the list. - **Public element:** `-list-item>` - **Wireframe tag:** `-list-item-wireframe>` | Variable | Type | Example | |---|---|---| | `record` | [`SomeRecord`](/api-reference/sdk/models/data-models#somerecord) | `` | | `record.user` | [`User`](/api-reference/sdk/models/data-models#user) | `` | | `index` | `number` | `velt-if="{index} === 0"` | | Property | Value | |---|---| | `shouldShow` | `record !== null` | {/* OPTIONAL — Deeply-Nested Wireframe Tags. A flat reference for the long tail of leaf tags the source documents at the bottom (icon fragments, label fragments, sub-row pieces). One subgroup per parent area. */} ## Deeply-Nested Wireframe Tags Each tag below has its own `-...-wireframe>` registration and inherits context variables from its parent. ### Header sub-components | Tag | Notes | Example | |---|---|---| | `-header-title-wireframe>` | Header title text. | `` | | `-header-close-button-wireframe>` | Close button. | (composes an icon + click target) | ### List-item sub-components | Tag | Notes | Example | |---|---|---| | `-list-item-avatar-wireframe>` | Avatar of the row's actor. | `` | | `-list-item-time-wireframe>` | Relative timestamp. | `` | ## Putting it together A typical row that highlights actions taken by the current user: ```jsx Wireframe.List.Item>
Wireframe.List.Item> ```
```html -list-item-wireframe>
-list-item-wireframe> ```
## Related - [ Wireframes](./) — composition reference for the wireframe tags themselves. - [ Primitives](./) — granular components if you don't need a full wireframe. - [Template Variables](/ui-customization/template-variables) — overview of the `velt-data` / `velt-if` / `velt-class` system. ```` *** ## 5. Authoring rules ### A. Frontmatter | Field | Rule | | ------------- | ------------------------------------------------------------------------------------------------------------ | | `title` | Exactly ` Wireframe Variables`. `` matches the `group` label in `docs.json`. | | `description` | One sentence. Must mention `velt-data`, `velt-if`, `velt-class` so the page indexes well for in-site search. | ### B. Variable tables — table-shape depends on context There are **two** table shapes used on the page. Match the right one to the context: **Top-level state buckets** (App State / Data State / UI State / Feature State / Component Config) and **Context-Specific Variables** — 4 columns: | Variable | Type | Description | Example | | -------- | ---- | ----------- | ------- | (Context-Specific uses `Available in` instead of `Description`.) **Subcomponent extra-variable tables** — 3 columns (no Description): | Variable | Type | Example | | -------- | ---- | ------- | The Description column is dropped at the subcomponent level because the rows are typically nested-access shorthands (`record.user`, `record.user.userId`) where the example tells you everything you need. ### C. Column rules (apply to both table shapes) | Column | Rule | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Variable** | Backticks around the name. Mapped features use bare names (`user`). Flat-config features use the full path (`componentConfig.user`). Nested-access rows in subcomponent tables use dot paths (`record.user.userId`). | | **Type** | Backticks. If compound (`User`, `CommentAnnotation`, `Comment`, `Notification`, `ActivityRecord`, etc.), make the type name a markdown link to `/api-reference/sdk/models/data-models#`. Union types: write outside the backticks and escape with `\|` if inside a table cell, e.g. `` [`Comment`](...) \| `null` ``. Array types: append `[]` outside the backticks, e.g. `` [`User`](...)`[]` ``. | | **Description** *(only on 4-col)* | One sentence. Start with the noun or verb — no leading "The …" / "A …". Use the source's "Notes" column when present; otherwise summarize the source's sample-value comment. | | **Example** | One usage form, chosen by type:
• booleans → `velt-if` / `velt-class`
• primitives & nested-access roots → `velt-data`
• arrays → `velt-data` with index
Use the HTML self-closing form (``) inline. The React/Other Frameworks tabs are reserved for the `` blocks. | ### D. Subcomponent Property/Value tables For each subcomponent, put metadata in a 2-column **Property | Value** table. Standard properties (omit any row that doesn't apply): | Property row | Use when | Example value | | ----------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `Extra variables` | Always include this row. | `None — root only sees common variables.` or `None.` or `None beyond common variables.` | | `shouldShow` | Source documents a `shouldShow` condition for the subcomponent. | `` `isEnabled === true && isOpen === true` `` | | `Host class` | Source documents a CSS class always applied to the host element. | `` `velt-activity-log--container` (always present) `` | If the subcomponent has extra variables, place the 3-col **Variable | Type | Example** table **above** the Property/Value table. ### E. Type Reference section Keep it slim. **Do NOT inline full type field tables** — that duplicates content from `data-models.mdx`. The section is just a quick lookup table: ```mdx theme={null} ## Type Reference Types referenced by the variables above are documented in [Data Models](/api-reference/sdk/models/data-models): | Type | Description | |---|---| | [`ActivityRecord`](/api-reference/sdk/models/data-models#activityrecord) | Core activity log object (id, featureType, actionType, actionUser, timestamp, etc.). | ``` The Description column is one short sentence — typically the type's purpose plus its 3–5 most-used fields in parentheses. Include the section only when the page references **2 or more** compound types. ### F. Sample values — use Accordion sparingly The source MD files include large JSON sample values per variable. **Do not paste these into the variable tables.** If a sample is genuinely useful (typically only for compound objects whose shape isn't obvious from the linked data-models page), put it in a collapsible `` immediately after the relevant table: ````mdx theme={null} ```json { "annotationId": "ann-123", "from": { "userId": "u-1", "name": "Mayank" }, "comments": [/* ... */] } ``` ```` **Default is omit.** The linked data-models page is the source of truth for shape. ### G. Code examples — drop wrapper elements Top-level rule: prefer attaching `velt-if` / `velt-class` directly to a real element rather than wrapping it in a structural container. **Don't** use these wrappers in either tab: ```html theme={null} ``` **Do** put `velt-if` directly on the element it controls: ```html theme={null} ``` If the conditional truly wraps multiple elements, use a real DOM element (`
`, ``, `
`) with `velt-if` as an attribute. ### H. "Putting it together" example * Always exactly one realistic wireframe combining 2–4 variables — enough to show real-world usage, not a kitchen sink. * Always wrapped in `` with two tabs titled exactly `"React / Next.js"` and `"Other Frameworks"` (matches sibling pages). * React tab uses the dot-notation wireframe component (e.g. ``) wrapping HTML that still uses `velt-if` / `velt-data` attributes — the tokens themselves are framework-agnostic. * Other Frameworks tab uses the kebab-case web component (e.g. ``). * Position: place this section **after** Subcomponents and Deeply-Nested Wireframe Tags, **before** Related. It's the closing example. ### I. Related links End every page with a `## Related` section linking to: 1. The sibling wireframes page — relative path matching the actual filename in this folder (`./wireframes` or `./-wireframes`). 2. The sibling primitives page — relative path matching the actual filename (`./primitives` or `./-primitives`). 3. `/ui-customization/template-variables` (absolute path). This closes the loop between the three pages of every feature group. ### J. Navigation registration (`docs.json`) After authoring the page, register it in navigation: 1. Open `docs.json`. 2. Find the `group` matching the feature name (e.g. `"group": "Activity Logs"`) under the **UI Customization** navigation tab. 3. Add the new page path to its `pages` array, between the existing `wireframes` and `primitives` entries: ```json theme={null} { "group": "Activity Logs", "pages": [ "ui-customization/features/async/activity-logs/activity-logs-wireframes", "ui-customization/features/async/activity-logs/activity-logs-wireframe-variables", "ui-customization/features/async/activity-logs/activity-logs-primitives" ] } ``` The same group label appears in multiple places in `docs.json` (Async Collaboration features, REST API, UI Customization). Make sure you edit the **UI Customization** entry, not the others. ### K. Forbidden terminology — never use these words The public docs are framework-neutral. The source MD files come from an Angular-heavy SDK codebase and use vocabulary that leaks framework details — strip it. **Do not use any of these words anywhere on the page:** | Don't write | Why | Use instead | | ------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Angular` | Framework-specific name. | "parent-child component composition", or omit the framework reference entirely. | | `` | Angular structural-directive container. | Drop the wrapper; put `velt-if` directly on the conditional element (see §G). | | `slot` / `sub-slot` | Conflates with Web Components `` semantics; not what we mean here. | "tag" (for the markup-level meaning of ``) or "sub-component" (for the conceptual subdivision). Use "values" if "slot" was being used in the sense of "field/property". | If a source MD file uses any of these words, translate them as part of authoring. *** ## 6. Anti-patterns — explicitly do NOT do these * ❌ **Do not rename the four state buckets**, even if the source mixes them. Keep `App State`, `Data State`, `UI State`, `Feature State` exactly as-is. Fold any "Per-instance Local UI State" rows into `UI State` — do not add a fifth bucket. * ❌ **Do not inline full type field tables** in the Type Reference section. Link out to `data-models.mdx`; only summarize. * ❌ **Do not restate template-variable mechanics** (operators, path resolution, `safeEval`, nested-access rules). Link to `template-variables.mdx` instead — DRY. * ❌ **Do not add a CSS variables section.** That's covered by `styling.mdx` and feature-level `variables.mdx` (CSS-only) pages. * ❌ **Do not paste large JSON sample values directly into variable tables.** Use an `` only when the shape isn't obvious from the linked type. * ❌ **Do not invent variable names.** If a name isn't in the source MD file, it's not supported — leave it out. * ❌ **Do not use forbidden terminology** (§K): no `Angular`, no ``, no `slot`/`sub-slot`. * ❌ **Do not wrap a single `` in a `` or other structural container.** Put `velt-if` directly on the element (§G). *** ## 7. Authoring checklist Before merging a new wireframe-variables page, confirm: * [ ] Filename matches the sibling-files convention in the same folder (bare vs prefixed). * [ ] Frontmatter `title` matches the feature's `group` label in `docs.json`. * [ ] Mapped vs flat-config decision matches the table in §2. * [ ] Top-level state buckets use the 4-col table (`Variable | Type | Description | Example`). * [ ] Subcomponent extra-variable tables use the 3-col table (`Variable | Type | Example`). * [ ] Subcomponent metadata uses the 2-col Property/Value table with the standard rows. * [ ] All compound types in the **Type** column link to the correct `data-models` anchor. * [ ] Type Reference section (if present) links out and does not inline field tables. * [ ] Each row's **Example** uses the right form for its type (`velt-data` / `velt-if` / `velt-class`). * [ ] Code examples never use `` or `` as a single-element wrapper. * [ ] No forbidden terminology: search the page for `angular`, `ng-container`, `slot` (case-insensitive) — must return zero hits in prose, headings, and code. * [ ] Exactly one `## Putting it together` block exists, wrapped in `` with both framework variants, positioned after Subcomponents and Deeply-Nested Wireframe Tags. * [ ] `## Related` links resolve (relative paths match the sibling filenames; absolute `/ui-customization/template-variables`). * [ ] No `## Component Config` section coexists with the four state buckets — pick one. * [ ] No CSS variables on this page. * [ ] `docs.json` updated under the **UI Customization** group (not API Reference or Async Collaboration), between the feature's `wireframes` and `primitives` entries. # Agent Comments Source: https://velt.dev/docs/ai/agent-comments Let your AI agents leave comments and findings directly in Velt by calling the Comments REST APIs. ## What are agent comments? Agent comments let your AI agents participate in collaboration the same way humans do — by leaving comments and findings anchored to a document. Instead of building a separate UI for agent output, your agent calls the Velt [Comment Annotations REST APIs](/docs/api-reference/rest-apis/v2/comments-feature/comment-annotations/add-comment-annotations) to create annotations that render in the standard Velt comments experience. Agent comments are built on top of the same [Comments](/docs/async-collaboration/comments/overview) feature, but they render with a special UI: each agent finding is a [suggestion](/docs/async-collaboration/suggestions/overview) with **Accept** and **Reject** buttons on the comment dialog. When a reviewer accepts or rejects a finding, the outcome is emitted on the comment element as the `suggestionAccepted` / `suggestionRejected` events, so you can apply the change to your own data or trigger follow-up logic. Any agent that can make an HTTP request can do this — a custom agent you register in the Velt Console, or an external agent running in your own framework (LangChain, CrewAI, a cron job, etc.). ## How it works 1. **Your agent runs** and produces a finding (a spelling error, an accessibility issue, a code-review note, etc.). 2. **Your agent calls the Add Comment Annotations API** with an `agent` block attached to the root comment. The server stamps `sourceType: "agent"` on both the comment and the annotation, and generates the annotation-level agent block. 3. **The finding renders in Velt** as a suggestion that humans can review, accept, or reject. 4. **You read agent annotations back** with the Get Comment Annotations API using agent-specific filters (`agentId`, `executionId`, `agentSource`, and more). ## The `agent` block Attach an `agent` object to the root comment (`commentData[0]`). It is discriminated on `agentSource`: | Field | Required | Description | | ------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `agentSource` | Yes | Origin of the agent. One of `velt` or `external`. | | `agentId` | Required for `velt`, optional for `external` | A custom agent ID verified server-side. Opaque (never validated) for `external` agents. | | `agentName` | Required for `external` | Display name for the agent. The only source of truth for an external agent's name. (For `velt` agents the name is resolved server-side.) | | `executionId` | No | Execution / run ID for this agent invocation. | | `url` | No | Page URL associated with the finding. | | `reason` | Yes | Finding details (`title`, `description`, `severity`, `findingType`, `confidence`, `suggestedFix`, etc.). Custom fields are preserved. | Set the annotation `type` to `"suggestion"` so the finding is classified as an agent suggestion rather than a regular comment. ### The `reason` object The `reason` object carries the finding's details. Here's each field with a description and an example value. | Field | Required | Type | Description | Example | | ------------------ | -------- | ------ | ------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `title` | ✅ | string | Short finding title — a quick label for the issue. | `"Low color contrast"` | | `description` | ✅ | string | Fuller explanation of what the agent found. | `"Contrast ratio is 2.1:1, below the 4.5:1 WCAG AA threshold."` | | `severity` | ✅ | string | How serious the finding is. One of `critical`, `high`, `medium`, `low`, `info`. | `"high"` | | `findingId` | — | string | Your own unique ID for the finding, useful for dedup/tracking. | `"finding_a11y_0427"` | | `findingType` | — | string | What kind of target the finding is on. One of `text`, `pin`, `page`. | `"pin"` | | `issueType` | — | string | Custom classification you define for your own taxonomy. | `"accessibility"` | | `confidence` | — | number | How confident the agent is. Integer 0–100. | `92` | | `suggestion` | — | string | Suggested change in plain text (human-readable advice). | `"Darken the button background to at least #1A1A1A."` | | `suggestedFix` | — | string | The concrete fix value to apply. | `"Welcome"` | | `htmlSnippet` | — | string | The relevant chunk of HTML where the issue lives. | `""` | | `htmlSelector` | — | string | CSS/HTML selector pointing to the finding's location. | `".cta-primary > button"` | | `source` | — | string | Where the triggering rule came from. One of `instructions`, `knowledge`. | `"knowledge"` | | `knowledgeSection` | — | string | Which knowledge section fired (pairs with `source: "knowledge"`). | `"brand-guidelines/accessibility"` | A note on the difference between the two "fix" fields, since it's easy to conflate them: `suggestion` is prose meant for a human to read in the comment, while `suggestedFix` is the actual replacement value. For a spelling correction, for instance, `suggestedFix` would be just `"Welcome"` — the corrected word itself, not a sentence about it. Putting it together, a fully-populated `reason` looks like: ```json theme={null} "reason": { "title": "Low color contrast", "description": "Contrast ratio is 2.1:1, below the 4.5:1 WCAG AA threshold.", "severity": "high", "findingId": "finding_a11y_0427", "findingType": "pin", "issueType": "accessibility", "confidence": 92, "suggestion": "Darken the button background to at least #1A1A1A.", "suggestedFix": "#1A1A1A", "htmlSnippet": "", "htmlSelector": ".cta-primary > button", "source": "knowledge", "knowledgeSection": "brand-guidelines/accessibility" } ``` Only the first three (`title`, `description`, `severity`) are required — everything else is optional, and any extra custom fields you add beyond this list are preserved by the server. ## Example: leave a comment from an external agent This is the most common path — an agent running in your own framework leaves a finding. Use `agentSource: "external"` and supply your own `agentName`. ```bash cURL theme={null} curl -X POST 'https://api.velt.dev/v2/commentannotations/add' \ -H 'x-velt-api-key: YOUR_API_KEY' \ -H 'x-velt-auth-token: YOUR_AUTH_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "data": { "organizationId": "acme-corp", "documentId": "design-mockup-v2", "commentAnnotations": [ { "type": "suggestion", "commentData": [ { "commentText": "This button has insufficient color contrast.", "from": { "userId": "a11y-bot" }, "agent": { "agentSource": "external", "agentName": "Accessibility Bot", "agentId": "a11y-bot", "executionId": "run_8f21", "url": "https://example.com/design-mockup-v2", "reason": { "title": "Low color contrast", "description": "Contrast ratio is 2.1:1, below the 4.5:1 WCAG AA threshold.", "severity": "high", "findingType": "pin" } } } ] } ] } }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.velt.dev/v2/commentannotations/add', { method: 'POST', headers: { 'x-velt-api-key': process.env.VELT_API_KEY, 'x-velt-auth-token': process.env.VELT_AUTH_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ data: { organizationId: 'acme-corp', documentId: 'design-mockup-v2', commentAnnotations: [ { type: 'suggestion', commentData: [ { commentText: 'This button has insufficient color contrast.', from: { userId: 'a11y-bot' }, agent: { agentSource: 'external', agentName: 'Accessibility Bot', agentId: 'a11y-bot', executionId: 'run_8f21', url: 'https://example.com/design-mockup-v2', reason: { title: 'Low color contrast', description: 'Contrast ratio is 2.1:1, below the 4.5:1 WCAG AA threshold.', severity: 'high', findingType: 'pin', }, }, }, ], }, ], }, }), }); const result = await response.json(); ``` ```python Python theme={null} import os import requests response = requests.post( "https://api.velt.dev/v2/commentannotations/add", headers={ "x-velt-api-key": os.environ["VELT_API_KEY"], "x-velt-auth-token": os.environ["VELT_AUTH_TOKEN"], "Content-Type": "application/json", }, json={ "data": { "organizationId": "acme-corp", "documentId": "design-mockup-v2", "commentAnnotations": [ { "type": "suggestion", "commentData": [ { "commentText": "This button has insufficient color contrast.", "from": {"userId": "a11y-bot"}, "agent": { "agentSource": "external", "agentName": "Accessibility Bot", "agentId": "a11y-bot", "executionId": "run_8f21", "url": "https://example.com/design-mockup-v2", "reason": { "title": "Low color contrast", "description": "Contrast ratio is 2.1:1, below the 4.5:1 WCAG AA threshold.", "severity": "high", "findingType": "pin", }, }, } ], } ], } }, ) result = response.json() ``` ## Reading agent comments back Use the [Get Comment Annotations API](/docs/api-reference/rest-apis/v2/comments-feature/comment-annotations/get-comment-annotations-v2) with agent-specific filters to fetch what your agents have left. Only one agent filter may be supplied per request. | Filter | Description | | ------------------ | ---------------------------------------------------------------- | | `agentId` | Annotations created by a specific agent. | | `executionId` | Annotations from a specific agent run. | | `agentSource` | `velt` or `external`. | | `agentSuggestions` | When `true`, returns only fresh (unaccepted) agent suggestions. | | `agentComments` | When `true`, returns all agent annotations regardless of status. | ```bash cURL theme={null} curl -X POST 'https://api.velt.dev/v2/commentannotations/get' \ -H 'x-velt-api-key: YOUR_API_KEY' \ -H 'x-velt-auth-token: YOUR_AUTH_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "data": { "organizationId": "acme-corp", "documentId": "design-mockup-v2", "agentId": "a11y-bot" } }' ``` Agent annotations in the response carry `type: "suggestion"` and `sourceType: "agent"` at the annotation root, an annotation-root `agent` block, and an `agent` block on each agent-authored comment (`comments[].agent`). The Get Comment Annotations API requires the **advanced queries** option to be enabled in the [Velt Console](https://console.velt.dev/dashboard/config/appconfig) and the v4 series of the Velt SDK. See the [API reference](/docs/api-reference/rest-apis/v2/comments-feature/comment-annotations/get-comment-annotations-v2) for details. ## Handle accept / reject in your app Because agent findings render with **Accept** and **Reject** buttons on the comment dialog, the outcome is emitted on the **comment element**. Subscribe to the [`suggestionAccepted` and `suggestionRejected` events](/docs/async-collaboration/comments/customize-behavior#event-subscription) to apply the change to your own data or trigger follow-up logic. The SDK records the outcome and persists the suggestion — applying the change is your code's job. ```tsx React theme={null} import { useCommentEventCallback } from '@veltdev/react'; export function AgentSuggestionListener() { const accepted = useCommentEventCallback('suggestionAccepted'); const rejected = useCommentEventCallback('suggestionRejected'); useEffect(() => { if (accepted) { // accepted.commentAnnotation contains the agent finding console.log('Suggestion accepted', accepted.commentAnnotation); } }, [accepted]); useEffect(() => { if (rejected) { console.log('Suggestion rejected', rejected.rejectReason); } }, [rejected]); return null; } ``` ```javascript Other Frameworks theme={null} const commentElement = Velt.getCommentElement(); commentElement.on('suggestionAccepted').subscribe(({ commentAnnotation }) => { // commentAnnotation contains the agent finding console.log('Suggestion accepted', commentAnnotation); }); commentElement.on('suggestionRejected').subscribe(({ commentAnnotation, rejectReason }) => { console.log('Suggestion rejected', rejectReason); }); ``` For the full suggestion lifecycle — statuses, applying `newValue`, and the other suggestion-stream events — see the [Suggestions](/docs/async-collaboration/suggestions/overview) guide. ## API reference * [Add Comment Annotations](/docs/api-reference/rest-apis/v2/comments-feature/comment-annotations/add-comment-annotations) — full reference for the Add API, including the `agent` block and agent suggestion examples. * [Get Comment Annotations](/docs/api-reference/rest-apis/v2/comments-feature/comment-annotations/get-comment-annotations-v2) — full reference for the Get API, including all agent filters and response field notes. * [Comment Events](/docs/async-collaboration/comments/customize-behavior#event-subscription) — subscribe to the `suggestionAccepted` and `suggestionRejected` comment events to react to accept/reject outcomes. # Customize Behavior Source: https://velt.dev/docs/ai/approval-engine/customize-behavior Node configuration, edge expressions, parallel groups, SLAs, linter rules, events, errors, and the full object reference. This is the deep-dive reference. Setup covers the happy path. This page covers everything you reach for when you need precise control over node behavior, edge routing, quorum policies, SLA breaches, and event consumption. ## Node configuration Every node has a `nodeId`, a `type` (`agent` or `human`), a `config` block, and an optional `slaMs` deadline. ### Agent nodes | Field | Type | Notes | | ----------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `agentId` | string | Required. | | `promptOverride` | string | `≤ 8000` chars. | | `inputMapping` | object | Pass step inputs to the agent. | | `blocking` | boolean | Default `false`. When `true`, the step parks in `waiting` until external resolutions arrive via `/steps/recordAgentResolution`. | | `resolutionPolicy` | object | Required when `blocking: true`. `{ kind: "allResolved" \| "minResolved", minCount?: integer }`. `minCount` is required when `kind === "minResolved"`. | | `agentMaxRuntimeMs` | integer | `≤ 86400000`. | | `requireNonEmptyOutput` | boolean | Fail the step if the agent returns an empty output. | ```json theme={null} theme={null} { "nodeId": "brand-check", "type": "agent", "config": { "agentId": "brand-agent-v1", "blocking": false, "requireNonEmptyOutput": true }, "slaMs": 3600000 } ``` ### Human nodes Exactly one of `reviewers[]` (preferred) or `reviewerIds[]` (legacy) must be provided. | Field | Type | Notes | | ---------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `reviewers` | array | Preferred: `[{ userId, mandatory }]`. Must include at least one `mandatory: true`. UserIds must be unique. | | `reviewerIds` | string\[] | Legacy. Accepted for back-compat only. | | `reviewerEmails` | string\[] | Optional. 0–50 reviewer email addresses. Surfaced in the human step's `output.reviewerEmails` for downstream notification UIs. | | `commentBody` | string | `≤ 8000` chars. Stored on the human step's output for use by your reviewer-facing UI. The engine does not auto-create a Velt annotation per human step in v1 — your application is responsible for surfacing this string to reviewers (and, if you use the legacy comment-resolution flow, for creating the comment thread the reviewer replies to). | ```json theme={null} theme={null} { "nodeId": "human-legal", "type": "human", "config": { "reviewers": [{ "userId": "u_legal_01", "mandatory": true }], "commentBody": "Please review for legal compliance." } } ``` ### Per-human-node `onReject` shorthand Authors can express the rejection path directly on a `human` node instead of declaring a top-level `loops[]` region or hand-writing an edge with `when: decision == 'reject'`. Two mutually-exclusive forms: **Form A — route on reject (synthesizes an edge):** ```json theme={null} theme={null} { "nodeId": "human-review", "type": "human", "config": { "reviewers": [{ "userId": "u1", "mandatory": true }], "onReject": { "routeToNodeId": "human-escalate" } } } ``` **Form B — loop back on reject (synthesizes a top-level loop region):** ```json theme={null} theme={null} { "nodeId": "human-review", "type": "human", "config": { "reviewers": [{ "userId": "u1", "mandatory": true }], "onReject": { "loopBack": { "toNodeId": "agent-draft", "maxIterations": 3, "onExhausted": { "routeToNodeId": "human-final-call" } } } } } ``` | Field | Type | Required | Description | | --------------------------------------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `onReject.routeToNodeId` | string | one of | Synthesizes an edge `{ from: , to: routeToNodeId, when: 'output.decision == \'reject\'' }`. | | `onReject.loopBack.toNodeId` | string | one of | Synthesizes a top-level [loop region](#loop-regions) with `entryNodeId = toNodeId`. Body inferred from the topological closure (forward-reachable from `toNodeId` ∩ backward-reachable to the rejecting node). | | `onReject.loopBack.maxIterations` | integer | no | Default 5. Range 1–20. | | `onReject.loopBack.onExhausted.routeToNodeId` | string | no | Same semantics as `LoopRegionDef.onExhausted.routeToNodeId`. | | `onReject.loopBack.when` | string (JSON-AST) | no | Custom predicate. Defaults to mandatory-reject. | ### Strict-mode validation Every `human` node MUST satisfy ONE of: 1. `config.onReject` is set (any form), OR 2. The node is a body member of a top-level `loops[]` entry. Otherwise the API returns `INVALID_ARGUMENT` with message `Human nodes missing a reject path: `. This forces explicit rejection routing — silent dead-ends on rejection are a bug. ### What desugaring rejects The `onReject` shorthand is converted to canonical `edges[]` + `loops[]` at definition write time. The pass returns `INVALID_ARGUMENT` for: | Condition | Why | | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `routeToNodeId` (or `loopBack.toNodeId` / `loopBack.onExhausted.routeToNodeId`) references a node that doesn't exist. | Target must be declared. | | The synthesized reject edge would duplicate an existing user-supplied edge. | Pick one — declare the edge yourself or use the shorthand. | | The rejecting node has an unconditional outgoing edge alongside `routeToNodeId`. | The unconditional edge would also fire on rejection. Add an explicit `when` (e.g. `decision == 'approve'`) to the forward edge, or switch to `loopBack`. | | `loopBack` body-inference returns empty (no path from `toNodeId` to this node along `edges[]`). | Customer must add edges or declare a top-level `loops[]` region. | | The rejecting node is already a body member of another top-level loop, OR the inferred body would overlap another loop's body. | A node may belong to at most one loop. Restructure or remove the conflicting loop. | | `loopBack.toNodeId` equals the node itself (degenerate self-loop). | No valid body can be inferred. | ### `onReject.routeToNodeId` on a `joinOnQuorum` group member is dead code The shorthand satisfies the strict-mode reject-path rule, but at runtime `joinOnQuorum` suppresses per-member fan-out — so the synthesized reject-gated edge can never fire. If you want per-rejecter routing on parallel reviewers, use `cancelOnQuorum` or `waitAll` (per-member fan-out preserved), or wrap the group in a top-level `loops[]` body so the loop interceptor handles rejections. ### Canonical form after desugaring The stored definition has `onReject` STRIPPED from human node configs and the synthesized edges/loops appended to the top-level arrays. `GET /definitions/get` returns the canonical form (auto-synthesized loops carry a `loopId` prefix `auto__loopback`). If you want to preserve your original input, store it on your side. ## Edge gating expressions Edges can carry an optional `when` expression evaluated against the source step's output. Expressions compile at write time (pure AST, no `eval`) and walk at runtime. No untrusted code ever runs. ```json theme={null} theme={null} { "from": "brand-check", "to": "legal-review", "when": "output.passesBrandCheck == true" } ``` **Supported operators:** equality, comparison, boolean, regex, `includes`, `startsWith`, `endsWith`, `length`, `isEmpty`. **Path roots:** | Root | Resolves to | | ------------------- | -------------------------------------------- | | `output.*` | The source step's `output` object. | | `step.*` | The source step's metadata (status, timing). | | `execution.input.*` | The `triggerContext` you passed on dispatch. | If `when` is omitted, the edge always fires. ## SLA and breach handling Set `slaMs` on any node to give the step a deadline. If the step doesn't complete within the window, it transitions to `breached` and emits a `step.breached` event. To handle breaches, declare an outgoing edge that routes on the breached status. Otherwise the engine emits the `missing-breach-edge` linter rule and rejects the definition. Silent dead-ends are a bug. ## Parallel groups and quorum policies A parallel group declares a set of member nodes that conceptually run in parallel and share an approval threshold. ```json theme={null} theme={null} { "groupId": "parallel-review", "memberNodeIds": ["human-legal", "human-brand"], "expectedSteps": 2, "quorum": 2, "onQuorumMet": "waitAll" } ``` | Field | Type | Required | Description | | ----------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `groupId` | string | yes | 1 to 64 chars. Stable identifier. | | `memberNodeIds` | string\[] | yes | 1 to 500 nodes. Each must be declared as a top-level node. A node may belong to at most one group. | | `expectedSteps` | integer | yes | 1 to 500. Total members the group expects to terminate (any status) before it considers itself "fully done." Should equal `memberNodeIds.length` in practice — values higher than the member count cause the group to never roll up to "complete." | | `quorum` | integer | yes | 1 to `expectedSteps`. The number of approvals required to fire the policy's side effect. | | `onQuorumMet` | enum | no | `waitAll` (default), `cancelOnQuorum`, or `joinOnQuorum`. | | `requiredNodeIds` | string\[] | no | Specific members whose approval is required for quorum to be met. Every entry must also be in `memberNodeIds`. `length` must be `<= quorum`. | ### Quorum is approval count, not completion count A member counts as an approval when its terminal status is `completed` AND its `output.decision === 'approve'`. Rejections, failures, breaches, and cancellations contribute to the completion counter only, never the approval counter. Two consequences: 1. Non-blocking agent members never satisfy quorum. They have no decision concept. Only place human or blocking agent nodes inside groups whose policy is `cancelOnQuorum` or `joinOnQuorum`. 2. A `reject` does not block the group from rolling up to "complete". It just keeps `approvedShards` from advancing. Group-completion (`expectedSteps` met) and group-quorum (approval threshold) are tracked separately. ### `onQuorumMet` policies | Policy | Side effect on first-time approval-quorum-met | Per-member fan-out | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `waitAll` (default) | Emits `group.quorum-met` event only. Group is purely informational. | Each member's outgoing edges fire on its own completion. If two members both fan out to the same downstream node, you get two downstream step instances. | | `cancelOnQuorum` | Emits `group.quorum-met` AND cancels every sibling member step still in `waiting` (system-actor cancellation, audit reason `group-quorum-met`). | Each completing member still fans out per-edge. Cancelled siblings do not fan out. | | `joinOnQuorum` | Emits `group.quorum-met`, cancels waiting siblings, AND fires a single group-owned downstream fan-out: one new step per shared outgoing-edge target with deterministic stepId `group___to__`. The successor's input is `{ groupOutputs, groupId, quorum, totalApproved }`. | Suppressed for group members. The group container owns fan-out, so downstream successors run exactly once. | ### Specific-must-approve quorum By default, quorum is anonymous: any N approvals out of M members trigger the policy. To express "these specific members must approve", declare them in `requiredNodeIds`: ```json theme={null} theme={null} { "groupId": "approver-group", "memberNodeIds": ["legal", "finance", "brand"], "expectedSteps": 3, "quorum": 2, "requiredNodeIds": ["legal", "finance"] } ``` Quorum-met now requires both: 1. Every `nodeId` in `requiredNodeIds` is among the approvers, AND 2. Total approval count reaches the numeric `quorum`. In the example, `brand` alone approving doesn't satisfy quorum even if `quorum: 2` is reached numerically. `legal` AND `finance` must both also approve. If `requiredNodeIds` is omitted or empty, behavior collapses back to anonymous quorum. ## Loop regions A loop region lets a workflow re-enter an earlier node when a reviewer rejects, instead of failing outright. Declare loops at the top level of a definition, peer to `groups[]`: ```json theme={null} theme={null} { "loops": [ { "loopId": "draft-revision", "entryNodeId": "agent-draft", "bodyNodeIds": ["agent-draft", "human-legal", "human-brand"], "onIterationReject": { "when": "{\"op\":\"and\",\"args\":[{\"op\":\"eq\",\"args\":[{\"var\":\"output.decision\"},\"reject\"]},{\"op\":\"eq\",\"args\":[{\"var\":\"output.rejectorMandatory\"},true]}]}" }, "onExhausted": { "routeToNodeId": "human-escalate" }, "maxIterations": 5 } ] } ``` | Field | Type | Required | Description | | --------------------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `loopId` | string | yes | 1–64 chars. Stable identifier for this loop. | | `entryNodeId` | string | yes | Node spawned first on each iteration. Must be a member of `bodyNodeIds`. | | `bodyNodeIds` | string\[] | yes | 1–50 nodes inside the loop's iteration scope. May include parallel-group members. | | `onIterationReject.when` | string (JSON-AST) | no | JSON-AST predicate evaluated when the loop body's iteration-terminal step finishes. If it matches, iteration N+1 spawns. Default: `decision == 'reject' && rejectorMandatory == true`. | | `onExhausted.routeToNodeId` | string | no | Node spawned when `maxIterations` is reached. Receives `previousAttempts` like a regular iteration entry. If omitted, the execution rolls up to `failed`. | | `maxIterations` | integer | yes | 1–20. Hard cap per execution. | ### Body-shape constraint The body must be one of: 1. **Single-terminal sequential.** Exactly one body node has outgoing edges that leave the body — that node is the iteration-terminal. 2. **Group-bounded.** The set of exit-bearing body nodes equals the `memberNodeIds` of one parallel group with `onQuorumMet: 'joinOnQuorum'`, every member lies inside the body, and the group has `quorum === expectedSteps`. The linter rejects other shapes with `loop-body-must-have-single-terminal`. ### When to use a loop vs. the `onReject` shorthand * **Use a loop region** when multiple parallel reviewers should share a single retry counter, when the retry zone needs to contain a `joinOnQuorum` group as a unit, or when you want explicit control over the `loopId`. * **Use the [`onReject` shorthand](#per-human-node-onreject-shorthand) on a single human node** for simple per-node reject routing (route to a different node on reject, or kick back to a single earlier node). ### `previousAttempts` payload threaded into iteration N+1 The entry step of iteration N+1 receives: ```ts theme={null} { iteration: number; // N+1 loopId: string; previousAttempts: Array<{ iteration: number; authorOutput: Record; // the body's iteration-terminal output rejectedBy: string; rejectorMandatory: boolean; rejectionReason: string | null; rejectedAt: number; }>; } ``` ### Iteration-scoped parallel groups When a parallel group lives inside a loop body, each iteration gets fresh quorum state — the container path becomes `parallelGroups/_iter_` internally. You don't address this directly, but it explains why per-iteration quorum starts from zero. ### Loop events Two new event types fire alongside the standard `step.*` and `execution.*` events. Both are returned from [Get Execution Events](/docs/api-reference/rest-apis/v2/approval-engine/executions/get-execution-events). | Event type | When emitted | `data` | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `loop.iteration-started` | Iteration N+1 spawns after a body iteration terminated rejected and the cap wasn't hit. | `{ loopId, iteration, triggeredBy: 'rejection' }` | | `loop.exhausted` | Cap reached. Either the `onExhausted.routeToNodeId` step is spawned next, or the execution rolls up to `failed` if no route node was declared. | `{ loopId, iteration, lastRejectedBy?, lastRejectionReason? }` | ## Linter rules Definitions are linted at create and update time. Any rule violation is rejected with `INVALID_ARGUMENT` and an explicit code in the error message. | Code | Meaning | | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `duplicate-node-id` | Two nodes share the same `nodeId`. | | `dangling-edge` | Edge references a `from` or `to` that isn't declared. | | `cycle-detected` | The graph contains a cycle. v1 is DAG-only. | | `unreachable-node` | A node has no path from any root. | | `node-missing-config` | A node has no config block. | | `missing-breach-edge` | A node has `slaMs` set but no outgoing edge that routes on `status == 'breached'`. Breaches would silently dead-end. | | `group-duplicate-id` | Two groups share the same `groupId`. | | `group-members-empty` | `memberNodeIds` is empty. | | `group-member-missing` | A member references an unknown node. | | `group-expected-steps-invalid` | `expectedSteps < 1`. | | `group-quorum-invalid` | `quorum < 1` or `quorum > expectedSteps`. | | `group-cancelonquorum-requires-quorum-lt-expected` | `cancelOnQuorum` requires `quorum < expectedSteps`. | | `group-joinonquorum-members-must-share-successors` | `joinOnQuorum` requires every member to have an identical set of outgoing-edge target nodes. | | `group-required-not-in-members` | An entry in `requiredNodeIds` is not in `memberNodeIds`. | | `group-required-exceeds-quorum` | `requiredNodeIds.length > quorum`. | | `group-node-in-multiple-groups` | A node appears as a member of two or more groups. | | `loop-duplicate-id` | Two loops share the same `loopId`. | | `loop-entry-must-be-in-body` | `entryNodeId` is not listed in `bodyNodeIds`. | | `loop-body-member-missing` | A `bodyNodeIds` entry isn't a declared node. | | `loop-body-unreachable-from-entry` | Some body node is unreachable from `entryNodeId` along body-internal edges. | | `loop-body-must-have-single-terminal` | Body shape is neither single-terminal sequential nor group-bounded — see [Loop regions › Body-shape constraint](#body-shape-constraint). | | `loop-node-in-multiple-loops` | A node appears in more than one loop body. A node may belong to at most one loop. | | `loop-on-exhausted-route-to-not-found` | `onExhausted.routeToNodeId` references an unknown node. | | `loop-on-exhausted-route-to-in-body` | `onExhausted.routeToNodeId` is itself a body node — exhausted-exit must escape the loop body. | | `loop-group-bounded-quorum-must-equal-expected` | Body is group-bounded but the bounding `joinOnQuorum` group has `quorum < expectedSteps`. Force `quorum === expectedSteps` so iteration-terminal coincides with all-members-done; otherwise a late rejection races against an already-fired join successor. | ## Events For receiver setup (signature verification, security rules, delivery basics), see [Setup, Configure your webhook receiver](/docs/ai/approval-engine/setup#step-3-configure-your-webhook-receiver). ### Event reference Externally-visible events delivered via webhook and returned from [Get Execution Events](/docs/api-reference/rest-apis/v2/approval-engine/executions/get-execution-events): | Event type (external) | Internal name | When emitted | `data` highlights | | ------------------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `execution.dispatched` | same | Execution created. First step(s) scheduled. | `{ definitionId, definitionVersion, rootStepIds }` | | `execution.completed` | same | All steps terminal, no unhandled failures. | null | | `execution.failed` | same | Any blocking step ended in `failed` or `breached` without a recovery edge. | `{ failureReason }` | | `execution.cancelled` | same | `/executions/cancel` or full-execution rollback. | `{ reason? }` | | `step.awaiting-approval` | `step.waiting` | A human or blocking-agent step entered `waiting`. | `{ waitingForReviewers, mandatoryCount, resumeKey }` | | `step.completed` | same | Step transitioned to `completed`. | For human or blocking-agent: `{ aggregatorStatus, nodeType, decision, aggregatorBacked }`. For non-blocking agents: `{ agentId }`. | | `step.failed` | same | Step transitioned to `failed` (retry budget exhausted). | `{ error: { code, message } }` | | `step.breached` | same | Step exceeded its configured SLA before completing. | `{ reason }` | | `step.cancelled` | same | Step cancelled via `/steps/cancel` or by quorum-met side effect. | `{ actorId, reason }` | | `group.quorum-met` | `parallel-group.quorum-met` | A parallel group's approval threshold was first satisfied. | `{ groupId, total, quorum, completedTotal, expectedSteps }` | | `loop.iteration-started` | same | Iteration N+1 spawns after a body iteration terminated rejected and the cap wasn't hit. | `{ loopId, iteration, triggeredBy: 'rejection' }` | | `loop.exhausted` | same | The loop's `maxIterations` cap was reached. If `onExhausted.routeToNodeId` is set, that step is spawned next; otherwise the execution rolls up to `failed`. | `{ loopId, iteration, lastRejectedBy?, lastRejectionReason? }` | Internal-only events (`step.scheduled`, `step.started`, `step.retried`, `step.resumed`, `step.response-recorded`, `step.overridden`, `parallel-group.completed`, `idempotency.suppressed`) fill `seq` gaps but are filtered from external delivery. Your stream may have non-contiguous `seq` values. ### Cancellation reasons `step.cancelled` events carry a `data.reason` string. This is an open string set. Consumers should switch on `event.type` for control flow, not on `data.reason`. | Reason | Source | Meaning | | ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `group-quorum-met` | system | Cancelled by the engine when the parent group's approval quorum was met under `cancelOnQuorum` or `joinOnQuorum`. Audit shows `actorId: "system:group-quorum"`. | | `loop-restart` | system | Cancelled by the engine when a [loop region](#loop-regions) is starting iteration N+1 and an in-flight body step from iteration N is still running. Audit shows `actorId: "system:loop-restart"`. | | (admin-supplied) | admin | Free-form reason passed to `/steps/cancel`. | ### Webhook retry policy | Attempt | Delay before retry | | ----------- | ----------------------- | | 1 (initial) | n/a | | 2 | 2 s | | 3 | 8 s | | 4 | 32 s | | 5 | 2 min | | 6 | 8 min, then dead-letter | After 5 failed retries, the payload is written to a dead-letter queue. Recover missed events via [Get Execution Events](/docs/api-reference/rest-apis/v2/approval-engine/executions/get-execution-events) with `sinceSeq`. **At-least-once delivery.** The same `eventId` and `seq` appear on retries. Make your receiver idempotent on `(executionId, seq)`. ## Errors All errors follow the standard envelope: ```json theme={null} theme={null} { "error": { "message": "...", "status": "INVALID_ARGUMENT", "details": {} } } ``` ### Canonical codes | Code | Meaning | Typical cause | | --------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `INVALID_ARGUMENT` | Schema or linter failure. | Missing field, wrong type, value out of range, linter rule violation. | | `UNAUTHENTICATED` | Missing or invalid `x-velt-auth-token`. | | | `PERMISSION_DENIED` | Auth token valid but lacks the required scope. | `/steps/resolve` with `reviewer-approve` / `reviewer-reject` and `actorId` not in the step's reviewer list. | | `NOT_FOUND` | Target doc does not exist. | Unknown `executionId`, `definitionId`, or `stepId`. | | `ALREADY_EXISTS` | Conflicting create. | Creating a definition with a `definitionId` already in use. | | `FAILED_PRECONDITION` | Optimistic lock or state-machine violation. | `ifVersion` mismatch on update. Cancelling a terminal step. Deleting a definition with in-flight executions. | | `RESOURCE_EXHAUSTED` | Rate limit exceeded. | Per-IP or per-API-key quota. | | `DEADLINE_EXCEEDED` | Internal timeout. | Retry with idempotency. | ### Schema-level validation errors | `message` | Trigger | | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `webhookUrl and webhookSecret must be provided together` | Dispatch supplied one but not the other. | | `webhookUrl must use https scheme` | Non-HTTPS scheme. | | `webhookUrl host resolves to a private, loopback, or link-local address` | Literal private IP, localhost, metadata.google.internal, or `*.internal`. | | `at least one of reviewerIds or reviewers must be provided` | Human node with no reviewers. | | `cannot set both reviewerIds and reviewers, use one` | Both populated. Pick the modern `reviewers[]` form. | | `reviewer userIds must be unique` | Duplicate userId in `reviewers[]`. | | `reviewers must include at least one mandatory reviewer (allMandatoryApproved would otherwise never resolve)` | Every `reviewer.mandatory === false`. | | `resolutionPolicy required when blocking === true` | Blocking agent node without a policy. | | `minCount required when kind === "minResolved"` | `resolutionPolicy.kind = "minResolved"` with no `minCount`. | ## Rate limiting Rate limits are applied per API key, with additional per-endpoint tiers on high-volume routes. A `RESOURCE_EXHAUSTED` error indicates you should back off with exponential retry. Dispatch retries are safe to replay with an `idempotencyKey`. ## Object reference ```typescript theme={null} theme={null} interface ExecutionView { executionId: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; startedAt: number; // epoch ms completedAt: number | null; cancelledAt: number | null; definitionId: string; definitionVersion: number; correlationId: string; idempotencyKey: string; failureReason: { code: string; message: string } | null; steps: StepView[]; } interface StepView { stepId: string; nodeId: string; nodeType: 'agent' | 'human'; status: 'pending' | 'running' | 'waiting' | 'completed' | 'failed' | 'skipped' | 'cancelled' | 'breached'; groupId: string | null; startedAt: number | null; completedAt: number | null; output: Record; error: { code: string; message: string } | null; } interface DefinitionView { definitionId: string; name: string; description: string | null; version: number; scope: { level: 'apiKey' | 'organization' | 'document'; organizationId: string | null; documentId: string | null }; nodes: NodeView[]; edges: EdgeView[]; groups: ParallelGroupDef[] | null; triggers: WorkflowTriggerConfig[] | null; tags: string[] | null; custom: Record | null; createdAt: number; updatedAt: number; status: 'active' | 'tombstoned'; } interface ApprovalEventView { eventId: string; seq: number; // monotonic per-execution type: string; // external event type, see Event reference stepId: string | null; timestamp: number; // epoch ms correlationId: string; data?: Record; } ``` For **human steps**, `output` (after resume) includes the aggregator rollup: ```typescript theme={null} theme={null} { reviewers: Array<{ userId: string; mandatory: boolean }>; reviewerIds: string[]; reviewerEmails: string[]; commentBody: string | null; aggregatorStatus: 'resolved' | 'rejected'; approveCount: number; rejectCount: number; totalResponses: number; mandatoryCount: number; mandatoryApproveCount: number; decision: 'approve' | 'reject'; approved: boolean; resumedAt: number; resumeKey: string; } ``` For **`joinOnQuorum` group successor steps**, `input` includes: ```typescript theme={null} theme={null} { groupOutputs: Record /* member's output */>; groupId: string; quorum: number; totalApproved: number; } ``` # Approval Engine (Beta) Source: https://velt.dev/docs/ai/approval-engine/overview Multi-step approval workflows where AI agents and humans review work together. ## What is the Approval Engine? Build approval flows as a graph. Define the workflow once, dispatch it against a trigger, and the engine runs everything: state, retries, fan-out, parallel review, SLAs, and webhooks. You write nodes (`agent`, `human`) and connect them with edges. You record reviewer decisions and agent resolutions through the REST API. The engine does the rest. Every state change is persisted, replayable, and (when you configure a webhook) pushed to your receiver in real time. This is the part of an approval system you don't want to build yourself: the runtime that keeps state, handles concurrent reviewers, enforces quorum, fires events, and survives outages. ## What you get * **Agent + human steps in one flow.** Run AI agents inline. Park them for human review when needed. Mix and match without writing glue code. * **Parallel review with quorum.** Run reviewers in parallel. Gate downstream work behind a quorum policy: wait for everyone, advance once N approve, or require specific people regardless of count. * **Real-time webhooks.** Every state change is POSTed to your receiver with an HMAC-SHA256 signature and exponential-backoff retry. Missed events are recoverable via `/executions/getEvents`. * **Idempotent dispatch.** Replay safely. Same `idempotencyKey`, same execution id, no duplicates. Even concurrent dispatches with the same key return the original id. * **SLA-aware.** Set a deadline per step. The engine transitions to `breached` and routes through dedicated `breached` edges. * **Versioned definitions.** Updates increment the version. In-flight executions stay pinned to the version they started on. * **Static linting.** Definitions are validated at write time for cycles, dangling edges, unreachable nodes, and quorum misconfiguration. No surprises at runtime. ## Use cases An AI agent drafts copy. Legal and brand reviewers approve in parallel. A publish agent ships the asset once both approve. Park a blocking agent step until N external resolutions arrive, then continue. Useful for agent-assisted moderation or QA pipelines where humans review specific outputs. Multi-stakeholder sign-off where specific reviewers (compliance, legal) must approve even if a numeric quorum is otherwise met. Every decision is auditable with correlation IDs. 3-of-5 quorum with `cancelOnQuorum` stops bothering remaining reviewers once the threshold is met. System-actor cancellation events go into the audit trail. Sequential or parallel approval chains with SLAs. If a step breaches its deadline, route to an escalation node or fail-fast the execution. Scope workflows per-document so each instance is bound to a specific `documentId`, with per-execution webhook receivers for downstream integrations. ## How it works 1. **Define the workflow.** Register a definition with nodes, edges, and optional parallel groups. The engine lints it at write time. 2. **Dispatch an execution.** POST a `definitionId`, an idempotency key, and optional `triggerContext` (exposed as `execution.input.*` in edge expressions). The engine pins the current version and enqueues the first step. 3. **The engine drives the flow.** Agent nodes run immediately. Human nodes (and blocking agent nodes) park in `waiting` until decisions arrive. 4. **Record decisions.** Call `/steps/recordReviewerDecision` for humans and `/steps/recordAgentResolution` for blocking agents. Matching edges fire. Quorum policies trigger their side effects. 5. **Consume events.** Subscribe via webhook for real-time delivery. Poll `/executions/getEvents` with `sinceSeq` to reconcile after an outage. ## Mental model ### Definition A **definition** is the blueprint. An **execution** is one live run of that blueprint. A **step** is one node executing inside an execution. ### Nodes Work units that can run. | Type | What it does | Parks in `waiting`? | | ------- | --------------------------------------------------------------------------------------------------------- | -------------------------- | | `agent` | Runs an agent. Non-blocking by default. With `blocking: true`, parks until external resolutions arrive. | Only when `blocking: true` | | `human` | Requires reviewer approval. Use `reviewers: [{ userId, mandatory }]`. Legacy `reviewerIds[]` still works. | Yes | ### Edges Connections between nodes. Edges optionally carry a `when` expression like `output.passesBrandCheck == true`. Expressions compile at write time (pure AST, no `eval`) and walk at runtime. Supported operators: equality, comparison, boolean, regex, `includes`, `startsWith`, `endsWith`, `length`, `isEmpty`. Path roots: `output.*`, `step.*`, `execution.input.*`. ### Execution An execution is one live run of a definition. Dispatch pins the definition version, stamps a correlation ID and idempotency key, and enqueues the first step(s). **Lifecycle:** ``` pending → running → completed | failed | cancelled ``` ### Step A step is one runtime instance of a node. **Lifecycle:** ``` pending → running → (waiting) → completed | failed | skipped | cancelled | breached ``` `waiting` only applies to human steps and blocking agent steps. Step IDs are deterministic, so retries land on the same doc: * **Root steps** (no incoming edges): `step___` * **Per-edge fan-out**: `${parentStepId}__to__${childNodeId}` * **`joinOnQuorum` fan-out**: `group___to__` (one instance regardless of how many group members ran) ## Scope Pick the level that matches how your product is structured. | Level | Bound to | | -------------- | ------------------------------------ | | `apiKey` | Workspace-wide | | `organization` | A specific `organizationId` | | `document` | A specific `documentId` under an org | Defaults to `{ level: "apiKey" }`. ## Idempotency Dispatch is idempotent on `idempotencyKey`. Replay with the same key returns `{ deduplicated: true, executionId: }`. Safe to retry from clients and queues. ## Events and webhooks Every state change writes an event doc with a monotonic `seq`. Set `webhookUrl` and `webhookSecret` on dispatch and externally-visible events are POSTed to your receiver with an HMAC-SHA256 signature and exponential-backoff retry. Recover missed events via `/executions/getEvents` with `sinceSeq`. ## Get started Author a workflow definition, dispatch an execution, configure your webhook receiver, and record decisions end-to-end. Node configuration, edge gating expressions, parallel groups and quorum policies, SLAs, linter rules, event reference, and the error vocabulary. # Setup Source: https://velt.dev/docs/ai/approval-engine/setup Build an approval flow end to end: define, dispatch, receive events, drive decisions. This guide walks through the five steps to get an approval flow running. Each step links to the full API reference for request and response details. ## Step 1: Define the workflow A definition is the static blueprint: nodes (work units), edges (connections), and optional parallel groups with quorum policies. Definitions are linted at write time. Cycles, dangling edges, unreachable nodes, and quorum misconfiguration all fail before you ever dispatch. You write one definition per workflow type (marketing copy approval, contract sign-off, etc.) and reuse it across many dispatches. → [Create Definition](/docs/api-reference/rest-apis/v2/approval-engine/definitions/create-definition) ## Step 2: Dispatch an execution Dispatching creates a live run of a definition. The engine pins the current definition version, stamps a correlation ID, and enqueues the first step. Always supply an `idempotencyKey`. Replays with the same key return the original `executionId` instead of spawning a duplicate. This makes dispatch safe to retry from clients, queues, and event handlers. Supply `webhookUrl` and `webhookSecret` here if you want real-time event delivery (covered in the next step). → [Dispatch Execution](/docs/api-reference/rest-apis/v2/approval-engine/executions/dispatch-execution) ## Step 3: Configure your webhook receiver When dispatch supplies `webhookUrl` and `webhookSecret`, every externally-visible state change is POSTed to your receiver. **Delivery:** POST, JSON body, 10s timeout, no redirects. **Headers your receiver sees:** | Header | What it is | | ------------------ | ---------------------------------------------------- | | `x-velt-signature` | `sha256=`. HMAC-SHA256 of the raw request body. | | `x-velt-event-id` | Stable event ID, unchanged across retries. | | `x-velt-attempt` | 0-based attempt counter. | ### Verify the signature Hash the raw request body bytes. Do not re-serialize the parsed JSON object. ```javascript Node.js theme={null} theme={null} const crypto = require('crypto'); function verifyVeltSignature(rawBody, headerValue, secret) { const [scheme, hex] = String(headerValue).split('='); if (scheme !== 'sha256' || !hex) return false; const computed = crypto .createHmac('sha256', secret) .update(rawBody, 'utf8') .digest('hex'); const a = Buffer.from(hex, 'hex'); const b = Buffer.from(computed, 'hex'); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` `webhookUrl` is validated at the schema boundary and re-checked at delivery time. Scheme must be `https://`. Literal IP hosts in loopback, private (RFC 1918), link-local, or IPv4-mapped-private ranges are rejected. Forbidden hostnames include `localhost`, `metadata.google.internal`, `metadata`, and any `*.internal`. At delivery, DNS resolution is repeated; if any resolved address is private, the request is not sent. Redirects are never followed. **At-least-once delivery.** The same `eventId` and `seq` appear on retries. Make your receiver idempotent on `(executionId, seq)`. For the full event catalog, see [Customize Behavior, Event reference](/docs/ai/approval-engine/customize-behavior#event-reference). ## Step 4: Record decisions When a human step parks in `waiting`, the engine emits `step.awaiting-approval`. Drive the workflow forward by recording each reviewer's decision. When all mandatory reviewers approve (or any reviewer rejects), the step's aggregator transitions terminal and the step resumes. → [Record Reviewer Decision](/docs/api-reference/rest-apis/v2/approval-engine/steps/record-reviewer-decision) For blocking agent steps, use Record Agent Resolution instead. → [Record Agent Resolution](/docs/api-reference/rest-apis/v2/approval-engine/steps/record-agent-resolution) ## Step 5: Monitor and reconcile Pull the current state of any execution at any time. → [Get Execution](/docs/api-reference/rest-apis/v2/approval-engine/executions/get-execution) If your receiver missed events during an outage, reconcile by calling Get Execution Events with `sinceSeq` set to the last `seq` you durably stored. Returns every event after that seq, in order. → [Get Execution Events](/docs/api-reference/rest-apis/v2/approval-engine/executions/get-execution-events) Only externally-visible event types are returned. Internal-only events fill `seq` gaps but are filtered out, so your stream may have non-contiguous `seq` values. ## End-to-end flows Marketing copy approval: agent drafts, legal and brand approve in parallel, publish agent ships once both approve. ``` 1. Create Definition → definition with parallel-review group, onQuorumMet: joinOnQuorum 2. Dispatch Execution → executionId returned, status=running webhook: execution.dispatched webhook: step.completed (agent-draft) webhook: step.awaiting-approval (human-legal) webhook: step.awaiting-approval (human-brand) 3. Record Reviewer Decision (u_legal_01, approve) webhook: step.completed (human-legal) 4. Record Reviewer Decision (u_brand_01, approve) webhook: step.completed (human-brand) webhook: group.quorum-met (parallel-review) [engine fires single group fan-out: creates step group_parallel-review__to__agent-publish] webhook: step.completed (agent-publish, single instance) webhook: execution.completed ``` `joinOnQuorum` fires one shared downstream step instead of firing it once per approver. Group with 3 reviewers, `quorum: 2`, `onQuorumMet: cancelOnQuorum`: ``` 2 of 3 approve → engine fires: webhook: group.quorum-met (parallel-review) webhook: step.cancelled (third-reviewer-step) data: { actorId: "system:group-quorum", reason: "group-quorum-met" } ``` The two approvers' downstream paths still fan out per edge. The cancelled third reviewer's edges do not fire. ## Next steps Node configuration, edge expressions, parallel groups, SLAs, linter rules, the event catalog, and the error vocabulary. All endpoints organized into Definitions, Executions, and Steps, with full request and response schemas. # Chat SDK Adapter Source: https://velt.dev/docs/ai/chat-sdk-adapter Build a bot that reads and responds to Velt comment threads using the Chat SDK and @veltdev/chat-sdk-adapter. [Chat SDK](https://chat-sdk.dev) lets you build cross-platform bots whose logic stays platform-agnostic. The [`@veltdev/chat-sdk-adapter`](https://www.npmjs.com/package/@veltdev/chat-sdk-adapter) connects a Chat SDK bot to **Velt comment threads**, so the same bot can run on Velt, Slack, Discord, and every other Chat SDK adapter. When a user @-mentions your bot or reacts in a comment thread, Velt sends a webhook to your app and the bot responds by replying in the thread, following the conversation, and reacting to events. **Try it live:** open the [tiptap comments demo](https://sample-apps-tiptap-comments-demo.vercel.app), leave a comment, and @-mention **Velt Bot**, and it streams an AI reply back into the thread. Source: the [`nextjs-velt-ai-bot`](https://github.com/velt-js/velt-chat-sdk-adapter/tree/main/examples/nextjs-velt-ai-bot) sample app. ## How it maps | Chat SDK | Velt | | --------------- | -------------------------------------------- | | Thread | Comment annotation inside a document | | Message | Comment | | Channel | Document | | `onNewMention` | A comment that @-mentions the bot | | `onReaction` | A reaction added to / removed from a comment | | `thread.post()` | A reply added to the comment thread | ## Quickstart ```bash theme={null} npm install @veltdev/chat-sdk-adapter chat @chat-adapter/state-memory ``` Create a `.env.local` file with your Velt API key, the bot's auth token, and the webhook secret. You'll get the webhook secret in the final step. ```env .env.local theme={null} VELT_API_KEY="your-velt-api-key" VELT_AUTH_TOKEN="" VELT_WEBHOOK_SECRET="whsec_..." VELT_ORGANIZATION_ID="your-organization-id" ``` `VELT_AUTH_TOKEN` is optional; if omitted, the adapter generates a bot token from your API key, scoped to `VELT_ORGANIZATION_ID`. The `resolveUsers` function converts Velt user ids into display names. ```tsx app/database.ts theme={null} export const BOT_USER_ID = "velt-bot"; export const BOT_USER_NAME = "Velt Bot"; const USERS = [ { userId: "user-1", name: "Charlie Layne" }, { userId: "user-2", name: "Mislav Abha" }, { userId: BOT_USER_ID, name: BOT_USER_NAME }, ]; export function getUser(userId: string) { const user = USERS.find((u) => u.userId === userId); return user ? { name: user.name } : undefined; } export function resolveUsers({ userIds }: { userIds: string[] }) { return userIds.map((id) => getUser(id)); } ``` Create a Chat instance with the Velt adapter and register handlers for mentions and reactions. ```tsx app/bot.ts theme={null} import { Chat } from "chat"; import { createMemoryState } from "@chat-adapter/state-memory"; import { createVeltAdapter, type VeltAdapter } from "@veltdev/chat-sdk-adapter"; import { BOT_USER_ID, BOT_USER_NAME, resolveUsers } from "./database"; let chatSingleton: Chat<{ velt: VeltAdapter }> | null = null; export function getChat() { if (chatSingleton) return chatSingleton; const chat = new Chat<{ velt: VeltAdapter }>({ userName: BOT_USER_NAME, adapters: { velt: createVeltAdapter({ botUserId: BOT_USER_ID, botUserName: BOT_USER_NAME, organizationId: process.env.VELT_ORGANIZATION_ID, resolveUsers, }), }, state: createMemoryState(), }); // Reply when a user @-mentions the bot. chat.onNewMention(async (thread, message) => { await thread.subscribe(); await thread.post(`Hi ${message.author.fullName}! How can I help?`); }); // React to reactions (read-only on the managed backend). chat.onReaction(async (event) => { console.log( `${event.user.fullName} ${event.added ? "added" : "removed"} ${event.emoji}`, ); }); chatSingleton = chat; return chat; } ``` The bot is created lazily so importing the module doesn't require credentials at build time. Credentials are read on the first webhook request. The bot processes incoming comments and reactions through this route. It must run on the Node.js runtime because signature verification needs the raw body and Node's `crypto`. ```tsx app/api/webhooks/velt/route.ts theme={null} import { after } from "next/server"; import { getChat } from "../../../bot"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function POST(request: Request) { return getChat().webhooks.velt(request, { waitUntil: (p) => after(() => p) }); } ``` On Vercel, use `waitUntil` from `@vercel/functions` instead of `after`. 1. Expose your endpoint publicly (e.g. with a tunnel during development). 2. In the Velt Console → **Configurations → Webhook Service**, set the endpoint URL and enable these events: * `comment.add` * `comment_annotation.add` * `comment.reaction_add` * `comment.reaction_delete` 3. Copy your **webhook secret** (`whsec_...`) into `VELT_WEBHOOK_SECRET`. You can also configure the webhook via [`POST /v2/workspace/webhookconfig/update`](/docs/api-reference/rest-apis/v2/workspace/webhookconfig-update). Now when users @-mention your bot or react to messages in comment threads, your bot responds automatically. ## Webhook versions The adapter supports both Velt webhook systems: * **Advanced (v2):** verified with Svix-style HMAC-SHA256 using the `whsec_...` secret. This is the default. * **Basic (v1):** verified against the `Authorization: Basic ` header. Set `webhookVersion: "v1"` and pass that token as `webhookSecret`. ## Reactions Reading reactions (`onReaction`) works on all Velt plans. **Writing** reactions (`addReaction` / `removeReaction`) is not supported on the managed Velt backend, because there is no managed REST endpoint to add a reaction as a user, so these methods throw a clear error. To enable reaction writes, pass a `selfHostingConfig.reactionsService` (a self-hosted reactions service backed by your own database) and the adapter delegates to it. ## What to read next * [Live demo: tiptap comments](https://sample-apps-tiptap-comments-demo.vercel.app) (@-mention **Velt Bot**) * Sample apps: [AI streaming bot](https://github.com/velt-js/velt-chat-sdk-adapter/tree/main/examples/nextjs-velt-ai-bot) · [greeting bot](https://github.com/velt-js/velt-chat-sdk-adapter/tree/main/examples/nextjs-velt-bot) * [Chat SDK documentation](https://chat-sdk.dev) * [`@veltdev/chat-sdk-adapter` on npm](https://www.npmjs.com/package/@veltdev/chat-sdk-adapter) * [Velt Webhooks](/docs/webhooks/advanced) * [Velt REST API: Comments](/docs/api-reference/rest-apis/v2/comments-feature/comments/add-comments) # Customize Behavior Source: https://velt.dev/docs/ai/rewriter/customize-behavior # addComment Create a comment annotation anchored to the text range from a [`TextSelectedEvent`](/docs/api-reference/sdk/models/data-models#textselectedevent). * **Params:** [`RewriterAddCommentRequest`](/docs/api-reference/sdk/models/data-models#rewriteraddcommentrequest) * **Returns:** `Promise<`[`RewriterAddCommentResponse`](/docs/api-reference/sdk/models/data-models#rewriteraddcommentresponse)`>` ```js theme={null} const rewriterElement = client.getRewriterElement(); const result = await rewriterElement.addComment({ text: 'Consider simplifying this.', event }); if (result.success) { console.log('Created annotation:', result.annotationId); } ``` ```js theme={null} const rewriterElement = Velt.getRewriterElement(); const result = await rewriterElement.addComment({ text: 'Consider simplifying this.', event }); if (result.success) { console.log('Created annotation:', result.annotationId); } ``` # askAi Send a text-generation prompt to any AI model via Velt's proxy. The provider is resolved automatically from the model name prefix (`gpt-*`/`o1-*`/`o3-*`/`o4-*` → OpenAI, `claude-*` → Anthropic, `gemini-*` → Gemini). * **Params:** [`RewriterAskAiRequest`](/docs/api-reference/sdk/models/data-models#rewriteraskairequest) * **Returns:** `Promise<`[`RewriterAskAiResponse`](/docs/api-reference/sdk/models/data-models#rewriteraskairesponse)`>` ```js theme={null} const rewriterElement = client.getRewriterElement(); const response = await rewriterElement.askAi({ model: 'gemini-2.5-flash', prompt: 'Make it more formal', selectedText: event.text, }); if (response.success) { console.log('AI output:', response.text); } ``` ```js theme={null} const rewriterElement = Velt.getRewriterElement(); const response = await rewriterElement.askAi({ model: 'gemini-2.5-flash', prompt: 'Make it more formal', selectedText: event.text, }); if (response.success) { console.log('AI output:', response.text); } ``` # enableRewriter ```js theme={null} const rewriterElement = client.getRewriterElement(); rewriterElement.enableRewriter(); rewriterElement.disableRewriter(); //to disable ``` ```js theme={null} if (Velt) { const rewriterElement = Velt.getRewriterElement(); rewriterElement.enableRewriter(); rewriterElement.disableRewriter(); //to disable } ``` # on Subscribe to Rewriter events. Supports `'textSelected'` to receive a [`TextSelectedEvent`](/docs/api-reference/sdk/models/data-models#textselectedevent) whenever a user selects text in a Rewriter-enabled region. * **Params:** `action: 'textSelected'` * **Returns:** `Observable<`[`TextSelectedEvent`](/docs/api-reference/sdk/models/data-models#textselectedevent)`>` ```js theme={null} const rewriterElement = client.getRewriterElement(); rewriterElement.on('textSelected').subscribe((event) => { console.log('Selected text:', event.text); }); ``` ```js theme={null} const rewriterElement = Velt.getRewriterElement(); rewriterElement.on('textSelected').subscribe((event) => { console.log('Selected text:', event.text); }); ``` # replaceText Replace the DOM text identified by a [`TextSelectedEvent`](/docs/api-reference/sdk/models/data-models#textselectedevent) with new text. * **Params:** [`RewriterReplaceTextRequest`](/docs/api-reference/sdk/models/data-models#rewriterreplacetextrequest) * **Returns:** `Promise<`[`RewriterReplaceTextResponse`](/docs/api-reference/sdk/models/data-models#rewriterreplacetextresponse)`>` ```js theme={null} const rewriterElement = client.getRewriterElement(); const result = await rewriterElement.replaceText({ text: 'Replacement text', event }); if (result.success) { console.log('Replaced:', result.originalText, '→', result.replacedText); } ``` ```js theme={null} const rewriterElement = Velt.getRewriterElement(); const result = await rewriterElement.replaceText({ text: 'Replacement text', event }); if (result.success) { console.log('Replaced:', result.originalText, '→', result.replacedText); } ``` # enableDefaultUI Show or hide the built-in Velt rewriter toolbar. Calling `disableDefaultUI()` suppresses the toolbar on text selection while keeping all rewriter events active, so you can render a fully custom UI. Call `enableDefaultUI()` to restore the default toolbar. * **Returns:** `void` ```js theme={null} const rewriterElement = client.getRewriterElement(); // Show the default Velt toolbar on text selection (default state) rewriterElement.enableDefaultUI(); // Hide the default Velt toolbar; handle UI yourself rewriterElement.disableDefaultUI(); ``` ```js theme={null} const rewriterElement = Velt.getRewriterElement(); // Show the default Velt toolbar on text selection (default state) rewriterElement.enableDefaultUI(); // Hide the default Velt toolbar; handle UI yourself rewriterElement.disableDefaultUI(); ``` # Rewriter Source: https://velt.dev/docs/ai/rewriter/overview Let users select text on your website and use AI to rewrite, improve, or comment on it — powered by any major LLM provider.
``` #### data-velt-pdf-viewer To support comments on top of a pdf viewer, add the `data-velt-pdf-viewer="true"` attribute in the container element of the pdf viewer. ```html theme={null}
``` #### svgAsImg * By default, Velt SDK treats SVGs as layered elements. * If you want to treat SVGs as flat images, you can use this. * Default: `false` **Using Props:** ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableSvgAsImg(); commentElement.disableSvgAsImg(); ``` **Using Props:** ```html theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableSvgAsImg(); commentElement.disableSvgAsImg(); ``` # Keyboard Controls #### enableHotkey Whether Hotkeys are enabled or not. For now, the only hotkey supported is pressing `c` to enable `comment mode`. `Default: false` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableHotkey(); commentElement.disableHotkey(); ``` **Using Props:** ```html theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableHotkey(); commentElement.disableHotkey(); ``` #### enableEnterKeyToSubmit * By default, pressing `enter` will add a new line and pressing `shift` + `enter` will submit a comment. * You can change this default behavior so that pressing `enter` will submit a comment by setting the `enterKeyToSubmit` property to `true`. **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableEnterKeyToSubmit(); commentElement.disableEnterKeyToSubmit(); ``` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableEnterKeyToSubmit(); commentElement.disableEnterKeyToSubmit(); ``` #### enableDeleteOnBackspace * Use this to enable or disable deleting comments when backpsace key is pressed. Default: `enabled` **Using Props:** ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableDeleteOnBackspace(); commentElement.disableDeleteOnBackspace(); ``` **Using Props:** ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableDeleteOnBackspace(); commentElement.disableDeleteOnBackspace(); ``` # Moderation #### enableModeratorMode Whether comments require moderator approval. `Default: false` By default, when a user adds a comment it is visible to all authenticated users on the same `document`. Moderator mode makes visibility of all comments private to only `admin` users and the comment author. Admin users will see an approve button on the comment dialog. Once approved the comment will be visible to all users who can access the `document`. You can set some users as `admin` by setting the `isAdmin` property in the User object, during the `identify()` call. ```js theme={null} ``` ```js theme={null} ``` API Method: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableModeratorMode(); commentElement.disableModeratorMode(); ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableModeratorMode(); commentElement.disableModeratorMode(); ``` #### enableResolveStatusAccessAdminOnly * Restrict the resolve action to admin users and the comment author only. **Using Props:** ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); // To enable resolve status access admin only commentElement.enableResolveStatusAccessAdminOnly(); // To disable resolve status access admin only commentElement.disableResolveStatusAccessAdminOnly(); ``` **Using Props:** ```html theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); // To enable resolve status access admin only commentElement.enableResolveStatusAccessAdminOnly(); // To disable resolve status access admin only commentElement.disableResolveStatusAccessAdminOnly(); ``` #### approveCommentAnnotation * Approves a comment annotation in moderator mode * Params: [ApproveCommentAnnotationRequest](/docs/api-reference/sdk/models/data-models#approvecommentannotationrequest) * Returns: [ApproveCommentAnnotationEvent](/docs/api-reference/sdk/models/data-models#approvecommentannotationevent) ```jsx theme={null} const approveCommentAnnotationRequest = { annotationId: 'ANNOTATION_ID' }; // Hook const { approveCommentAnnotation } = useApproveCommentAnnotation(); const approveCommentAnnotationEvent = await approveCommentAnnotation(approveCommentAnnotationRequest); // API Method const commentElement = client.getCommentElement(); const approveCommentAnnotationEvent = await commentElement.approveCommentAnnotation(approveCommentAnnotationRequest); ``` ```js theme={null} const approveCommentAnnotationRequest = { annotationId: 'ANNOTATION_ID' }; const commentElement = Velt.getCommentElement(); const approveCommentAnnotationEvent = await commentElement.approveCommentAnnotation(approveCommentAnnotationRequest); ``` #### acceptCommentAnnotation * Accepts a comment annotation in suggestion mode * Params: [AcceptCommentAnnotationRequest](/docs/api-reference/sdk/models/data-models#acceptcommentannotationrequest) * Returns: [AcceptCommentAnnotationEvent](/docs/api-reference/sdk/models/data-models#acceptcommentannotationevent) ```jsx theme={null} const acceptCommentAnnotationRequest = { annotationId: 'ANNOTATION_ID' }; // Hook const { acceptCommentAnnotation } = useAcceptCommentAnnotation(); const acceptCommentAnnotationEventData = await acceptCommentAnnotation(acceptCommentAnnotationRequest); // API Method const commentElement = client.getCommentElement(); const acceptCommentAnnotationEventData = await commentElement.acceptCommentAnnotation(acceptCommentAnnotationRequest); ``` ```js theme={null} const acceptCommentAnnotationRequest = { annotationId: 'ANNOTATION_ID' }; const commentElement = Velt.getCommentElement(); const acceptCommentAnnotationEventData = await commentElement.acceptCommentAnnotation(acceptCommentAnnotationRequest); ``` #### rejectCommentAnnotation * Rejects a comment annotation in suggestion mode * Params: [RejectCommentAnnotationRequest](/docs/api-reference/sdk/models/data-models#rejectcommentannotationrequest) * Returns: [RejectCommentAnnotationEvent](/docs/api-reference/sdk/models/data-models#rejectcommentannotationevent) ```jsx theme={null} const rejectCommentAnnotationRequest = { annotationId: 'ANNOTATION_ID' }; // Hook const { rejectCommentAnnotation } = useRejectCommentAnnotation(); const rejectCommentAnnotationEventData = await rejectCommentAnnotation(rejectCommentAnnotationRequest); // API Method const commentElement = client.getCommentElement(); const rejectCommentAnnotationEventData = await commentElement.rejectCommentAnnotation(rejectCommentAnnotationRequest); ``` ```js theme={null} const rejectCommentAnnotationRequest = { annotationId: 'ANNOTATION_ID' }; const commentElement = Velt.getCommentElement(); const rejectCommentAnnotationEventData = await commentElement.rejectCommentAnnotation(rejectCommentAnnotationRequest); ``` #### enableSuggestionMode Whether to enable suggestion mode to accept or reject comments. `Default: false` To accept comments, set the `suggestionMode` attribute to `true`. ```js theme={null} ``` To accept comments, set the `suggestion-mode` attribute to `true`. ```html theme={null} ``` API Method: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableSuggestionMode(); commentElement.disableSuggestionMode(); ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableSuggestionMode(); commentElement.disableSuggestionMode(); ``` #### enableReadOnly Control whether comments are in read-only mode. When enabled, any features requiring user interaction (e.g., Composer, Reactions, Status) will be removed. Default: `false` Using Props: ```jsx theme={null} ``` Using API: ```js theme={null} const commentElement = client.getCommentElement(); commentElement.enableReadOnly(); commentElement.disableReadOnly(); ``` Using Props: ```html theme={null} ``` Using API: ```js theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableReadOnly(); commentElement.disableReadOnly(); ``` # Comment Read Status #### enableSeenByUsers Control whether the "Seen By" feature is enabled for comments. When enabled, it shows which users have seen each comment. Default: `true` **Using Props:** ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableSeenByUsers(); commentElement.disableSeenByUsers(); ``` **Using Props:** ```html theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableSeenByUsers(); commentElement.disableSeenByUsers(); ``` #### setUnreadIndicatorMode Whether `verbose` mode is enabled for unread `Comments`. `Default: 'minimal'` Unread `Comments` can be in `minimal` mode or `verbose` mode. In `minimal` mode, a small red dot indicator appears for unread `Comments`. In `verbose` mode, a larger badge with the text "UNREAD" will appear for unread `Comments`. ```jsx theme={null} ``` ```jsx theme={null} ``` API Method: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.setUnreadIndicatorMode("verbose"); // use badge with text UNREAD commentElement.setUnreadIndicatorMode("minimal"); // use small red dot indicator ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.setUnreadIndicatorMode("verbose"); // use badge with text UNREAD commentElement.setUnreadIndicatorMode("minimal"); // use small red dot indicator ``` # Toggle Comment Types #### enableAreaComment Area comments allows users to draw a rectangle and attach a comment to it. Use this to enable or disable area comments. Default: `true` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableAreaComment(); commentElement.disableAreaComment(); ``` **Using Props:** ```jsx theme={null} ``` Using API Method: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableAreaComment(); commentElement.disableAreaComment(); ``` #### enablePopoverMode For a complete setup guide for Popover mode, [read here](/docs/async-collaboration/comments/setup/popover). Whether Popover Mode is enabled. Default: `false` ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enablePopoverMode(); commentElement.disablePopoverMode(); ``` ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enablePopoverMode(); commentElement.disablePopoverMode(); ``` #### enableStreamMode For a complete setup guide for Stream mode, [read here](/docs/async-collaboration/comments/setup/stream). Whether Stream Mode is enabled. Default: `false` ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableStreamMode(); commentElement.disableStreamMode(); ``` **Using Props:** ```html theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableStreamMode(); commentElement.disableStreamMode(); ``` #### enableTextMode For a complete setup guide for Text mode, [read here](/docs/async-collaboration/comments/setup/text). Whether Text Mode is enabled. Default: `true` **Using Props:** ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableTextComments(); commentElement.disableTextComments(); ``` **Using Props:** ```html theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableTextComments(); commentElement.disableTextComments(); ``` #### enableInlineCommentMode Whether In-line comment mode is enabled. When In-line comment mode is enabled, comments will appear under the text they are associated with in the DOM, instead of as a pop up window. Default: `false` ```jsx theme={null} ``` ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableInlineCommentMode(); commentElement.disableInlineCommentMode(); ``` ```jsx theme={null} ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableInlineCommentMode(); commentElement.disableInlineCommentMode(); ``` #### enableMultithread * By default comments are single threaded. * You can make it multithreaded by setting `multiThread` prop to `true`. * If you had previously used a wireframe for the comment dialog, you will need to add the [multithread wireframe](/docs/ui-customization/features/async/comments/multithread-comment-dialog). * Default: `false` ```jsx theme={null} ``` ```html theme={null} ``` # Comment Tool #### context * Add `context` to the `Velt Comment Tool` component to associate custom metadata with comments created using that tool. * Predefine context directly within the component itself. * Currently, this feature is specific to popover comments. This allows you to, for example, assign unique context to each cell in a table if you place a `Velt Comment Tool` in each cell. * The `context` prop accepts an object with key-value pairs. ```jsx theme={null} // For popover comments ``` ```html theme={null} ``` #### contextInPageModeComposer Pass context data to the page mode composer when opening the comment sidebar via the comment tool. Context automatically clears when the sidebar closes. API Methods: [`enableContextInPageModeComposer()`](/docs/api-reference/sdk/api/api-methods#enablecontextinpagemodecomposer), [`disableContextInPageModeComposer()`](/docs/api-reference/sdk/api/api-methods#disablecontextinpagemodecomposer) * Params: `boolean` * Returns: `void` For programmatically setting context data, see [`setContextInPageModeComposer()`](#setcontextinpagemodecomposer). ```jsx theme={null} // Using props import { VeltCommentTool } from '@veltdev/react'; // Using API methods const commentElement = client.getCommentElement(); commentElement.enableContextInPageModeComposer(); commentElement.disableContextInPageModeComposer(); ``` ```html theme={null} ``` #### enableCommentMode Turns Comment mode on or off. When you click on the comment tool, it turns on comment mode and user can attach comment to any element on the DOM. Using this method you can programmatically turn on the commenting mode. ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableCommentMode(); commentElement.disableCommentMode(); ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableCommentMode(); commentElement.disableCommentMode(); ``` #### onCommentModeChange The comment mode is toggled on and off when you click on the Comment Tool. The `useCommentModeState()` hook can be used to get the Comment mode without having to subscribe to changes. When the Comment mode changes, the hook return value will update. The subscription is automatically unsubscribed when the component dismounts. ```jsx theme={null} import { useCommentModeState } from "@veltdev/react"; export default function YourDocument() { let commentModeState = useCommentModeState(); return
Comment Mode is turned on: {commentModeState}
; } ```
To subscribe to changes in the comment mode, use the `onCommentModeChange()` method , as a property on `VeltCommentTool`: ```jsx theme={null} onCommentModeChange(mode)} /> ``` API method: ```jsx theme={null} let subscription = commentElement.onCommentModeChange().subscribe((mode) => { //mode contains the state after change }); ``` To unsubscribe from the subscription: ```jsx theme={null} subscription?.unsubscribe(); ``` API method: ```jsx theme={null} const commentElement = Velt.getCommentElement(); let subscription = commentElement.onCommentModeChange().subscribe((mode) => { //mode contains the state after change }); ``` To unsubscribe from the subscription: ```jsx theme={null} subscription?.unsubscribe() ```
#### enableCommentTool Whether the Comment Tool button is Enabled. `Default: true` When the Comment Tool is disabled, it can not be used to leave comments. Other ways to leave comments, such as highlighting text, will also be disabled. **Using Props:** ```jsx theme={null} ``` Using API methods: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableCommentTool(); commentElement.disableCommentTool(); ``` **Using Props:** ```html theme={null} ``` **Using API methods:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableCommentTool(); commentElement.disableCommentTool(); ``` #### disabled Disables the comment tool and prevents users from adding new comments. This is helpful when you want to temporarily or conditionally restrict comment creation while still allowing users to view existing comments. Default: `false` This prop disables the specific comment tool instance it is applied to; the `disableCommentTool()` API disables all comment tools globally. ```jsx theme={null} ``` ```html theme={null} ``` #### enableChangeDetectionInCommentMode * By default, DOM Change Detection is disabled in Comment Mode for better performance. * You can enable it to automatically reposition comment pins when the DOM changes while in Comment Mode. `Default: false` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableChangeDetectionInCommentMode(); commentElement.disableChangeDetectionInCommentMode(); ``` **Using Props:** ```html theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableChangeDetectionInCommentMode(); commentElement.disableChangeDetectionInCommentMode(); ``` #### enablePersistentCommentMode * When Persistent comment mode is enabled, you can continue leave additional comments after finishing a comment. * When it is disabled, you will need to reclick the Comment Tool every time when you want to make a comment. Default: `false` ```jsx theme={null} ``` ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enablePersistentCommentMode(); commentElement.disablePersistentCommentMode(); ``` ```html theme={null} ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enablePersistentCommentMode(); commentElement.disablePersistentCommentMode(); ``` #### forceCloseAllOnEsc * When enabled, pressing the ESC key will force close persistent comment mode even if a comment thread is currently active. * When disabled (default), pressing ESC will only close the active comment thread but keep persistent comment mode enabled. * This provides more control over the ESC key behavior in persistent comment mode. Default: `false` ```jsx theme={null} ``` ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableForceCloseAllOnEsc(); commentElement.disableForceCloseAllOnEsc(); ``` ```jsx theme={null} ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableForceCloseAllOnEsc(); commentElement.disableForceCloseAllOnEsc(); ``` #### setPinCursorImage You can set custom mouse cursor when the comment mode is on. The custom cursor image must be **32 x 32 pixels**. ```jsx theme={null} ``` ```jsx theme={null} ``` API Methods: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.setPinCursorImage(BASE64_IMAGE_STRING); ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.setPinCursorImage(BASE64_IMAGE_STRING); ``` # Minimap #### enableMinimap * The minimap shows a bar on the edge of the screen with indicators that show where comments exist. * Use this to enable/disable the minimap. By default it's disabled. * It can be positioned `left` or `right`. By default, it's positioned on the right side of the screen. **Option a. Enable using config:** ```jsx theme={null} ``` **API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableMinimap(); commentElement.disableMinimap(); ``` ```jsx theme={null} ``` **API Method:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableMinimap(); commentElement.disableMinimap(); ``` **Option b. Enable using Minimap Component:** This offers greater flexibility to customize and position the minimap. ```jsx theme={null}
{/* scrollable content */}
```
```jsx theme={null}
```
# Inline Comments Section #### sortBy and sortOrder * Change the default sorting order of Comments in the Inline Comments Section. * Params: * `sortBy`: The field to sort by. Currently supports `createdAt` and `lastUpdated`. Default: `lastUpdated` for multithread and `createdAt` for single thread. * `sortOrder`: The order to sort by. It can be `asc` or `desc`. Default: `desc` for multithread and `asc` for single thread. ```jsx theme={null} ``` ```html theme={null} ``` #### multiThread * By default [inline comment section](/docs/async-collaboration/comments/setup/inline-comments) is multithreaded. * You can make it single threaded by setting `multiThread` prop to `false`. * Default: `true` ```jsx theme={null} ``` ```html theme={null} ``` #### commentPlaceholder, replyPlaceholder, composerPlaceholder * Customize placeholder text for different input fields in the inline comments section. * Props: * `commentPlaceholder`: Placeholder text for the main comment input field. * `replyPlaceholder`: Placeholder text for the reply input field. * `composerPlaceholder`: Placeholder text for the composer input field. ```jsx theme={null} ``` ```html theme={null} ``` To customize placeholder text shown when **editing** an existing comment or reply, use `editPlaceholder`, `editCommentPlaceholder`, and `editReplyPlaceholder`. See [`VeltInlineCommentsSectionProps`](/docs/api-reference/sdk/models/data-models#veltinlinecommentssectionprops) for details. #### composerPosition * Change the position of the comment composer in the inline comments section to `top` or `bottom`. * Default: `bottom` ```jsx theme={null} ``` ```html theme={null} ``` #### context * Pass a custom context object to the Inline Comments Section component. * The provided context will be added to any comments created in the inline comments section, allowing you to associate custom metadata with those comments. * Additionally, the component will filter and display only the comments that match the provided context object. * This dual behavior makes it easy to scope comments to specific areas of your application (e.g., specific cells in a table, dashboard widgets, or data segments). ```jsx theme={null} ``` ```html theme={null} ``` #### contextOptions * Configure the matching behavior for the context object when filtering comments. * By default, comments must fully match all key-value pairs in the provided context. * Set `partialMatch: true` to enable flexible matching where comments match if they contain all the specified context fields (extra fields in the comment's context are ignored). * See [`Context` Matching](#context-matching) for detailed explanation and examples. ```jsx theme={null} {/* Full match (default) - comment must have exact context */} {/* Partial match - comment matches if it contains these fields */} ``` ```html theme={null} ``` #### readOnly Control read-only mode at the component level. When enabled, users can view comments but cannot reply, edit, or add new comments in the Inline Comments Section. The local component prop takes precedence over global settings when explicitly set. Default: `false` ```jsx theme={null} ``` ```html theme={null} ``` # Popover Comments #### enableDialogOnTargetElementClick Whether the comment dialog opens when target element is clicked. This is relevant only for Popover mode. `Default: true` ```js theme={null} ``` ```js theme={null} ``` API Method: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableDialogOnTargetElementClick(); commentElement.disableDialogOnTargetElementClick(); ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableDialogOnTargetElementClick(); commentElement.disableDialogOnTargetElementClick(); ``` #### enablePopoverTriangleComponent Whether the popover triangle appears when Popover Mode is enabled. `Default: true` ```jsx theme={null} ``` ```jsx theme={null} ``` API Method: ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enablePopoverTriangleComponent(); commentElement.disablePopoverTriangleComponent(); ``` ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enablePopoverTriangleComponent(); commentElement.disablePopoverTriangleComponent(); ``` # Comment Bubble #### annotationId * The id of the comment annotation to show the comment bubble on. * The bubble will be rendered only if there is a comment annotation that matches the provided `annotationId` in the current document. ```jsx theme={null} ``` ```html theme={null} ``` #### targetElementId * The DOM ID of the element where comment bubble is added. * This binds the comment bubble to the element with the provided ID. * The bubble will be rendered only if there is a comment annotation that was added to the element with the provided ID. ```jsx theme={null} ``` ```html theme={null} ``` #### context * The `context` object to filter which comment annotations to show the comment bubble for. * The bubble will be rendered only if there is a comment annotation that matches the provided `context` in the current document. * Works only with `popover` mode comments. * Perfect for complex tables with filtering and segmentation needs. * Set flexible comment anchoring and filtering logic at the cell level using key-value pairs. * **Supports aggregate views:** Eg: comments added in day view can appear in week/month views automatically. * Context commonly has combined use with [context options](#contextoptions) and the [group matched comments](#groupmatchedcomments) feature. ```jsx theme={null} // Full match // Partial match ``` ```html theme={null} ``` #### contextOptions * Matching behavior for the context object (default: full match, or set `partialMatch: true` for flexible matching). * **How Partial Match Works:** * A comment will match if ALL provided filter criteria exist in the comment's context * Extra fields in the comment's context don't prevent matching * Missing fields in the comment's context prevent matching * Example: Comment has `{ day: "01", week: "01", month: "jan", product: "cheese", location: "zurich" }` * Filter `{ day: "01", product: "cheese" }` → ✅ matches (both fields exist in comment) * Filter `{ day: "01", category: "dairy" }` → ❌ no match (category doesn't exist in comment) * **Partial Match Examples:** * Comment has `{ day: "01", week: "01", month: "jan", product: "cheese" }` * Filter with `{ day: "01", week: "01", month: "jan", product: "cheese" }` → matches (full) * Filter with `{ week: "01", month: "jan", product: "cheese" }` → matches (partial) * Filter with `{ day: "01", week: "01", month: "jan", product: "cheese", location: "zurich" }` → no match #### groupMatchedComments Whether to group multiple comment annotations in Comment Bubble component when multiple annotations match the provided `context` or `targetElementId`. Default: `false` **Using Props:** ```jsx theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableGroupMatchedComments(); commentElement.disableGroupMatchedComments(); ``` **Using Props:** ```html theme={null} ``` **Using API:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableGroupMatchedComments(); commentElement.disableGroupMatchedComments(); ``` #### commentCountType Whether to show unread or total comment replies count on Comment Bubble Component. Type: [`CommentCountType`](/docs/api-reference/sdk/models/data-models#commentcounttype) * `total`: Shows the total number of replies. (default) * `unread`: Shows the number of unread replies. ```jsx theme={null} ``` ```jsx theme={null} ``` #### openDialog Control whether the comment dialog opens when clicking on the comment bubble. When disabled, clicking the bubble will not open the comment dialog. This is useful when you want to handle bubble clicks with custom logic using the [`commentBubbleClicked`](#commentbubbleclicked) event. Default: `true` ```jsx theme={null} ``` ```html theme={null} ``` #### readOnly The `readOnly` flag prevents users from replying or editing existing comments in the target bubble while still displaying them. This is useful when you want to display comments in a read-only mode where users can view but not modify or respond to comments. Default: `false` ```jsx theme={null} ``` ```html theme={null} ``` # Video Timeline Comments #### setTotalMediaLength Set the total length of media (in frames or seconds) for the timeline. **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.setTotalMediaLength(120); ``` **Using Props:** ```html theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.setTotalMediaLength(120); ``` #### offset * Allows comment bubbles to be positioned relative to both parent and child video clips by specifying an offset value. * Default: `0` ```jsx theme={null} ``` ```html theme={null} ``` # Comment Pin #### enableBubbleOnPin Show a Comment Bubble when user hovers or clicks on the Comment Pin vs showing the Comment Dialog. The comment dialog will open only on clicking the comment bubble. `Default: 'false'` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = useCommentUtils(); commentElement.enableBubbleOnPin(); commentElement.disableBubbleOnPin(); ``` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); commentElement.enableBubbleOnPin(); commentElement.disableBubbleOnPin(); ``` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); commentElement.enableBubbleOnPin(); commentElement.disableBubbleOnPin(); ``` #### enableBubbleOnPinHover Show a Comment Bubble when user hovers on the Comment Pin vs clicks on it. `Default: 'true'` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = useCommentUtils(); // To enable/disable showing bubble on pin commentElement.enableBubbleOnPin(); commentElement.disableBubbleOnPin(); // To enable/disable showing bubble on hover commentElement.enableBubbleOnPinHover(); commentElement.disableBubbleOnPinHover(); ``` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = client.getCommentElement(); // To enable/disable showing bubble on pin commentElement.enableBubbleOnPin(); commentElement.disableBubbleOnPin(); // To enable/disable showing bubble on hover commentElement.enableBubbleOnPinHover(); commentElement.disableBubbleOnPinHover(); ``` **Using Props:** ```jsx theme={null} ``` **Using API Method:** ```jsx theme={null} const commentElement = Velt.getCommentElement(); // To enable/disable showing bubble on pin commentElement.enableBubbleOnPin(); commentElement.disableBubbleOnPin(); // To enable/disable showing bubble on hover commentElement.enableBubbleOnPinHover(); commentElement.disableBubbleOnPinHover(); ``` # Legacy Methods #### onCommentAdd Using Props: ```js theme={null} yourMethod(event)} /> const yourMethod = (event) => { event?.addContext({ customKey: 'customValue' }); } ``` Using Hooks: ```jsx theme={null} import { useCommentAddHandler } from "@veltdev/react"; export default function YourDocument() { const commentAddEvent = useCommentAddHandler(); useEffect(() => { console.log("commentAddEvent", commentAddEvent); }, [commentAddEvent]); return
; } ``` Using API: ```js theme={null} const commentElement = client.getCommentElement(); commentElement.onCommentAdd().subscribe((event) => { console.log("commentAddEvent", event); }); ```
Using Event listener: ```js theme={null} const veltCommentsTag = document.querySelector("velt-comments"); veltCommentsTag?.addEventListener("onCommentAdd", (event) => { console.log("*** onCommentAdd ***"); console.log(event.detail); event.detail?.addContext({ customKey: "customValue" }); }); ``` Using API method: ```js theme={null} const commentElement = Velt.getCommentElement(); commentElement.onCommentAdd().subscribe((event) => { event?.addContext({ customKey: "customValue" }); }); ```
**onCommentAdd Event Data Schema** | Field Name | Type | Description | | ------------------ | ----------------- | ------------------------------------------------------------------- | | addContext | Function | Use this to set custom data on the comment | | annotation | CommentAnnotation | The annotation that is associated with the comment that was updated | | documentId | string | The document ID where the comment was added | | location | Object | The location where the comment was added | | targetAnnotationId | string | The id of the target annotation | #### onCommentUpdate Using Props: ```js theme={null} yourMethod(event)} />; const yourMethod = (event) => { console.log("commentUpdateEvent", event); }; ``` Using Hooks: ```jsx theme={null} import { useCommentUpdateHandler } from "@veltdev/react"; export default function YourDocument() { const commentUpdateEvent = useCommentUpdateHandler(); useEffect(() => { console.log("commentUpdateEvent", commentUpdateEvent); }, [commentUpdateEvent]); return
; } ``` Using API: ```js theme={null} const commentElement = client.getCommentElement(); commentElement.onCommentUpdate().subscribe((event) => { console.log("commentUpdateEvent", event); }); ```
Using Event Listener: ```js theme={null} const veltCommentsTag = document.querySelector('velt-comments'); veltCommentsTag?.addEventListener('onCommentUpdate', (event) => { console.log('**_ onCommentUpdate _**'); console.log(event.detail); }); ``` Using API method: ```js theme={null} const commentElement = Velt.getCommentElement(); commentElement.onCommentUpdate().subscribe((event) => { console.log('commentUpdateEvent', event); }); ```
**onCommentUpdate Event Data Schema** | Field Name | Type | Description | | ------------------ | ----------------- | -------------------------------------------------------------------------- | | annotation | CommentAnnotation | The annotation that is associated with the comment that was updated | | type | string | The type of comment that was updated | | targetAnnotationId | string | The ID of the target annotation that contains the comment that was updated | | targetCommentId | number | The ID of the target comment that was updated | | updateContext | Function | Use this to update the custom metadata on the comment annotation. | #### getAllCommentAnnotations * Get all comment annotations for a given document and location. * By default, it will return data for the current `documentId` and `location`. * Params (optional): * `documentId`: string; it will return all comments in the given `documentId`. * `location`: Object; it will return all comments in the given `location`. **Using Hooks:** ```jsx theme={null} const commentAnnotations = useCommentAnnotations(); useEffect(() => { if (commentAnnotations) { console.log("commentAnnotations", commentAnnotations); } }, [commentAnnotations]); ``` **Using API:** ```js theme={null} const commentElement = client.getCommentElement(); let subscription = commentElement .getAllCommentAnnotations() .subscribe((comments) => { console.log("commentAnnotations", comments); }); ``` To unsubscribe from the subscription: ```jsx theme={null} subscription?.unsubscribe(); ``` ```js theme={null} if (Velt) { const commentElement = Velt.getCommentElement(); let subscription = commentElement.getAllCommentAnnotations().subscribe((comments) => { // Do something with comments }); } ``` To unsubscribe from the subscription: ```jsx theme={null} subscription?.unsubscribe() ``` # Notifications Source: https://velt.dev/docs/async-collaboration/comments/notifications There are several options to send notifications to your users. There are three ways to send notifications to your users: Add notifications component within your app. Send email notifications to your users. Send notifications to other channels like Slack. ## In-app notifications Add notifications component within your app. Learn more about [In-app notifications](/docs/async-collaboration/notifications/overview). ## Email notifications You can enable email notifications to send out emails whenever you `@mention` a user in the Comments feature or when another user replies to your comment. There are two ways to trigger email notifications: Webhooks and SendGrid. ### Webhooks for non-SendGrid services To learn how to trigger email notifications via Webhooks please refer [here](/docs/webhooks/basic). ### SendGrid Integration To enable Email Notifications, go to the Configurations -> Email Service in the Velt Console, or [click here](https://console.velt.dev/dashboard/config/email). For SendGrid integration, provide the following details: * SendGrid API Key * SendGrid Email Template ID for Comments feature * 'From' Email Address The 'From' Email Address needs to be whitelisted from your SendGrid account or else it will not work. #### Email Template Data The following fields are sent to Sendgrid: | Field | Type | Description | | -------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | firstComment | [Comment](/docs/api-reference/sdk/models/data-models#comment) | First message in the thread. **Only Contains** `commentId`, `commentText` and `from` properties derived from Comment class. | | latestComment | [Comment](/docs/api-reference/sdk/models/data-models#comment) | Latest message in the thread that prompted the email. **Only Contains** `commentId`, `commentText` and `from` properties derived from Comment class. | | prevComment | [Comment](/docs/api-reference/sdk/models/data-models#comment) | Previous message to the latestMessage. **Only Contains** `commentId`, `commentText` and `from` properties derived from Comment class. | | commentsCount | string | Total number of comments in the comment annotation | | commentsCountMoreThanThree | string | Total number of remaining comments in the comment annotation beyond the three that this payload contains | | fromUser | [User](/docs/api-reference/sdk/models/data-models#user) | Action user's object | | commentAnnotation | [CommentAnnotation](/docs/api-reference/sdk/models/data-models#commentannotation) | The comment annotation object without the `comments` field | | actionType | string | The action that resulted in the notification. You can find the list of action types [here](/docs/webhooks/basic#list-of-action-types) | | documentMetadata | [DocumentMetadata](/docs/api-reference/sdk/models/data-models#documentmetadata) | The document metadata object | These are the older fields that will be deprecated soon. These are already contained in the fields above: | Field | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | message | string | the message of the email | | messageFromName | string | the name of whoever wrote the message that the email is referencing. If name is not found then it will contain the email. | | name | string | the name of the person who performed the action that triggered the email. If name is not found then it will contain the email. Sometimes a notification can be triggered without a message. For those cases, you can use this. | | fromEmail | string | email address of the user who performed the action | | photoUrl | string | avatar URL of user | | pageUrl | string | url of the page the comment is on | | pageTitle | string | title of the web page | | deviceInfo | Object | contains browser, OS and device info | | subject | string | subject of the email | #### Sample Payload sent to SendGrid ```json expandable lines theme={null} { "fromName": "Tony via ", "fromEmail": "noreply@example.com", "replyTo": "tony@example.com", "templateId": "d-60ba77a7a42a4e55803487b40982b499", "toEmail": "jess@example.com", "properties": { "message": "@Jess", "messageFromName": "Tony", "name": "Tony", "fromName": "Tony via ", "fromEmail": "tony@trysnippyly.com", "photoUrl": "PHOTO_URL", "pageUrl": "https://example.com/?scommentId=4Fnt7zTvPv9ggE7OOTVG", "pageTitle": "Example Title", "deviceInfo": {...}, "subject": "added you to a comment on Example Title", "actionType": "newlyAdded", "fromUser": {...}, // User Object "commentAnnotation": {...}, // CommentAnnotation Object "commentsCount": 1, "firstComment": {...}, // Comment Object "prevComment": {...}, // Comment Object "commentsCountMoreThanThree": 0, "latestComment": {...}, // Comment Object "documentMetadata": {...} // DocumentMetadata Object } } ``` #### Download Sample Email Template We have provided this sample HTML email template you can use for your SendGrid email template: [Download Link](https://firebasestorage.googleapis.com/v0/b/snippyly.appspot.com/o/external%2Femail-template.html?alt=media\&token=16a17614-70ca-464c-b6f4-2b2b097b0007) ```html expandable lines theme={null} Email Template
@here testing here functionality
    
``` ## Webhooks Send notifications to other channels like Slack. Learn more about [Webhooks](/docs/webhooks/basic). # Comments Source: https://velt.dev/docs/async-collaboration/comments/overview Your users can add comments in context to ask questions, leave feedback, report bugs etc. We handle all complexity to ensure the comments are robust against content changes. We support many types of comment UX patterns as illustrated below. With `Freestyle` comments, you can pin `Comments` on any elements on the page or draw area comments. [Open in larger window](https://demo-examples.vercel.app/async/comments/area-comments?background=000\&theme=dark)     [View Setup for Freestyle Comments](/docs/async-collaboration/comments/setup/freestyle)