Comment on a design
This guide shows you how to discuss a design in comment threads. Comments are not facts explains what a thread attaches to and why no export holds it.
You need the Commenter role or a higher role to write. A Viewer reads the threads and cannot write. See Manage members.
The curl examples use the session cookie of a signed-in person in the file vellum.cookies. Use the HTTP API shows how to sign in. An agent token cannot use these endpoints: the answer is 403.
Start a thread on an element
Section titled “Start a thread on an element”- Open the project.
- On the canvas, select the element or the relationship.
- In the detail panel, go to Comments and select New comment.
- Write the comment and select Comment.
A marker with the number of open threads shows on the element on the canvas. Select the marker to open the threads of the element.
To start a thread on the project, select Comments in the toolbar on the left, then select On the project. The Comments panel lists the threads of the model that is shown, with the tabs Open, Resolved, Detached and All.
curl -X POST "http://localhost:8080/api/projects/<project-id>/threads" \ -b vellum.cookies \ -H "Content-Type: application/json" \ -d '{"target":{"kind":"element","id":"<element-id>"},"body":"Why is this call synchronous?"}'The answer is 201 with the thread. The thread has id, target (kind, id, stepId, name), modelVersion (the version of the model stream modelStreamId when the thread was made), modelStreamId, status, createdBy, createdAt and comments.
| Field | Meaning |
|---|---|
target.kind |
project, element, relationship, message, operation, channel, step, promise, document or decision |
target.id |
The stable ID of the target. For project, the project ID. For step, the scenario ID. |
target.stepId |
The step ID in the scenario, for step only. |
draftId |
Optional. The ID of an open draft. The target must exist on that draft. Leave it out for main. |
mentions |
Optional. The user IDs of the members that the text mentions. At most 20. |
id |
Optional. A UUID. A second request with the same ID gives the thread that exists. |
| Status | Cause |
|---|---|
400 |
The body is empty or too long, the kind is unknown, or no target with this ID exists on this branch. |
403 |
Your role is Viewer, or you are not a member of the workspace. |
409 |
The draft is merged or abandoned, or the ID is in use. |
Start a thread on a promise, a decision, a page or a flow step
Section titled “Start a thread on a promise, a decision, a page or a flow step”Each of these has a Comments section with the same controls.
- Promise: select Promises in the toolbar, open the promise, and go to Comments at the end of the dialog.
- Decision (ADR): select Review, then Decisions, open the decision, and go to Comments.
- Document page: open the page and select Comments in its header. The threads show beside the page.
- Flow step: select Scenarios, edit the scenario, and select the comment button beside the step ID. Only a saved step takes comments.
The lists of promises, decisions, documents and scenarios show the number of open threads beside each item. For a scenario, the number counts the threads on all its steps.
- Open the thread.
- Write the reply and select Reply.
curl -X POST "http://localhost:8080/api/projects/<project-id>/threads/<thread-id>/replies" \ -b vellum.cookies \ -H "Content-Type: application/json" \ -d '{"body":"Because the stock check must hold before the order is accepted."}'A reply is on one level. You cannot reply to a reply.
Mention a member
Section titled “Mention a member”- In the text, type
@. - Select the member in the list.
The member gets an entry in the inbox. You can mention each member of the workspace, also a Viewer. In the API, send the user IDs in mentions. A user ID that is not a member of the workspace gives 400.
Edit or delete a comment
Section titled “Edit or delete a comment”Only the author edits a comment. The comment then shows edited and the time. Vellum keeps no earlier text.
curl -X PATCH "http://localhost:8080/api/projects/<project-id>/threads/<thread-id>/comments/<comment-id>" \ -b vellum.cookies \ -H "Content-Type: application/json" \ -d '{"body":"Why is this call synchronous and not a message?"}'The author or an Owner deletes a comment. The comment stays in the thread with the text Comment deleted. When an Owner deletes the comment of another person, the audit log records comment.deleted-by-owner with the IDs of the comment, the thread, the project and the author.
curl -X DELETE "http://localhost:8080/api/projects/<project-id>/threads/<thread-id>/comments/<comment-id>" \ -b vellum.cookiesResolve or reopen a thread
Section titled “Resolve or reopen a thread”The author of the thread, an Editor or an Owner does this.
- Open the thread.
- Select Resolve. To open it again, select Reopen.
curl -X POST "http://localhost:8080/api/projects/<project-id>/threads/<thread-id>/resolve" -b vellum.cookiescurl -X POST "http://localhost:8080/api/projects/<project-id>/threads/<thread-id>/reopen" -b vellum.cookiesList the threads
Section titled “List the threads”GET /api/projects/{projectId}/threads gives the threads of main. Add draftId=<draft-id> for the threads of a draft, targetId=<id> for the threads of one target, and status=open, resolved or detached.
A thread whose target was deleted has the status detached and the last name of the target. It has no marker on the canvas. Find it on the tab Detached of the Comments panel.
Raise a thread as a question or a decision
Section titled “Raise a thread as a question or a decision”A thread can become an item of the review. The item cites the thread by its ID.
- Raise as open question needs the Commenter role. Vellum makes an open question with the text of the first comment and the citation. The question is about the target of the thread. A second request gives the same question.
- Raise as decision needs the Editor role. You give a title and the decision text. Vellum makes a proposed decision. Its context is the text of the first comment and the citation. Only an Owner accepts it. See Record a decision.
curl -X POST "http://localhost:8080/api/projects/<project-id>/threads/<thread-id>/raise-question" -b vellum.cookies
curl -X POST "http://localhost:8080/api/projects/<project-id>/threads/<thread-id>/raise-decision" \ -b vellum.cookies \ -H "Content-Type: application/json" \ -d '{"title":"Check stock with a synchronous call","decision":"The order service calls the stock service before it accepts an order."}'When the first comment is deleted, the thread has no text to raise, and the answer is 409.
Read the inbox
Section titled “Read the inbox”The inbox lists the comments that mention you, and the replies on threads that you started or replied to. You get no entry for your own comment.
- In the top bar, select the bell. The number is the count of unread entries.
- Select an entry to open the thread. The entry is then read.
- To mark all entries read, select Mark all read.
curl "http://localhost:8080/api/inbox" -b vellum.cookiescurl -X POST "http://localhost:8080/api/inbox/<item-id>/read" -b vellum.cookiescurl -X POST "http://localhost:8080/api/inbox/read-all" -b vellum.cookiesThe answer of GET /api/inbox has unread and items. Add unreadOnly=true for the unread entries only. The inbox shows no entry of a workspace that you are no longer a member of.
Send comment events to a webhook
Section titled “Send comment events to a webhook”An owner sets the webhook URL of the workspace in Settings → Notifications → Webhook URL. See Settings. Vellum then sends one POST for each of these events:
| Event | When |
|---|---|
comment.created |
A person starts a thread or replies. |
comment.mentioned |
A comment mentions members, or an edit mentions a member for the first time. |
comment.resolved |
A person resolves a thread. |
The body is JSON:
{ "event": "comment.created", "deliveryId": "<uuid>", "occurredAt": "2026-10-14T20:44:01.547957+00:00", "workspaceId": "<uuid>", "projectId": "<uuid>", "draftId": null, "threadId": "<uuid>", "commentId": "<uuid>", "author": { "userId": "<id>", "name": "Dev User" }, "target": { "kind": "element", "id": "<uuid>", "stepId": null, "name": "Orders API" }, "link": "http://localhost:8080/projects/<project-id>?thread=<thread-id>"}comment.mentioned also has mentioned, a list of userId and name. comment.resolved has no commentId, and author is the person who resolved the thread.
The body holds no comment text and no source text. A receiver that needs the text follows the link with a signed-in account.
link starts with Vellum__PublicUrl. With no such value, the hosted profile uses the host name of the request, and the local profile sends null. The Compose file sets it to http://localhost:8080. See Bootstrap environment.
The request has these headers:
X-Vellum-Event: the event.X-Vellum-Delivery: the delivery ID. A retry has the same ID.X-Vellum-Timestamp: the time of the send, in seconds since 1970-01-01 UTC.X-Vellum-Signature: only when the workspace has the secretnotifications:webhookSecret. It issha256=and the hex HMAC-SHA-256 of<timestamp>.<body>with the secret as the key.
To check a request, compute the HMAC of the timestamp, a dot and the raw body. Compare it with the header. Refuse a timestamp that is older than five minutes.
Vellum sends the event after the change commits. A 2xx answer delivers it. For any other answer or a network failure, Vellum tries again after 30 seconds, then after 1, 2, 4 and 8 minutes. After 6 tries, Vellum stops. Each try reads the URL again and applies the URL rules of the instance policy. An event of a workspace with no webhook URL is not kept.