> ## Documentation Index
> Fetch the complete documentation index at: https://docs.entire.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Comment on trails

> Post a comment on a trail and keep it current as your work progresses.

A plugin registered with `discussions:write` can start a discussion on a trail, reply to one, and edit or delete its own messages. The usual pattern is one summary comment per trail that your plugin keeps current. For example, a deploy-preview plugin posts a table of preview URLs when a [trail opens](https://docs.entire.com/guides/api/plugins/trail-events.md), and edits the same comment as each deployment finishes.

> [!NOTE]
>
> Trail comments are experimental. Field names may change before the contract is final.

## What you need

* **`discussions:write`**, to post and edit. Ask Entire to add it when you [register your plugin](https://docs.entire.com/guides/api/plugins/register.md). It grants nothing else: no read access to the repository, and no push.
* **`contents:read`**, to list a trail's discussions. You need it to find your comment again if you don't store its IDs.
* **An access token scoped to the repository**, with both permissions. [Mint it](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#mint-an-access-token) with this body:

  ```json theme={null}
  {"repository_id": "repo_01M3...", "permissions": {"contents": "read", "discussions": "write"}}
  ```

Send every call below to the repository's [`api_base_url`](https://docs.entire.com/guides/api/regions-and-routing), under `/api/v1`, with the access token as bearer token. The [Go SDK](#with-the-go-sdk) does the minting and routing for you.

## Post the comment

[Start a discussion](https://docs.entire.com/api-reference/trails/trail-discussions/start-a-new-discussion-repo_id-addressing.md) on the trail:

```http theme={null}
POST /api/v1/repos/{repo_id}/trails/{number}/discussions
Content-Type: application/json

{"title": "Deploy previews", "body": "| App | Status |\n| :-- | :-- |\n| web | Building |"}
```

The title is optional, up to 512 characters. The body is required, up to 65,536 characters, and entire.com renders it as Markdown. A `201` returns the new discussion and its first message:

```json theme={null}
{"discussion": {"id": "...", "title": "Deploy previews", ...}, "message": {"id": "...", "body": "...", ...}}
```

Store `discussion.id` and `message.id` with the trail. You need both to edit the comment.

## Keep it current

[Edit the message](https://docs.entire.com/api-reference/trails/trail-discussions/edit-a-message-repo_id-addressing.md) whenever your state changes. The new body replaces the old one, so send all of it:

```http theme={null}
PATCH /api/v1/repos/{repo_id}/trails/{number}/discussions/{discussion_id}/messages/{message_id}
Content-Type: application/json

{"body": "| App | Status |\n| :-- | :-- |\n| web | [Ready](https://preview.acme.example) |"}
```

Edit the comment you already posted rather than posting a new one for each update. You can also [reply in the discussion](https://docs.entire.com/api-reference/trails/trail-discussions/add-a-message-to-a-discussion-repo_id-addressing.md), and [delete a message](https://docs.entire.com/api-reference/trails/trail-discussions/delete-a-message-repo_id-addressing.md) that no longer applies.

## Find your comment again

If you lost the IDs, [list the trail's discussions](https://docs.entire.com/api-reference/trails/trail-discussions/list-a-trails-discussions-repo_id-addressing.md) with `GET /api/v1/repos/{repo_id}/trails/{number}/discussions`. While the response has a `next_cursor`, pass it back as `?cursor=` for the next page. Yours is the one whose `created_by_identity` names your installation:

```json theme={null}
"created_by_identity": {
  "kind": "automation",
  "account_id": null,
  "automation": {"plugin": "acme-ci", "installation_id": "ins_01M4..."}
}
```

Then [get the discussion](https://docs.entire.com/api-reference/trails/trail-discussions/get-a-discussion-and-its-messages-repo_id-addressing.md) for its messages. Each message's `identity` has the same shape. Match on `installation_id`, not on `author`: for a plugin, `author` is an empty string and `created_by` is null.

## Who can change your comment

A message belongs to the installation that posted it, not to the token. Any later token for the same installation can edit or delete it, so your comment stays editable for as long as the installation exists.

Nobody else can. Another installation of your plugin, or a person, gets `404` for your messages, and entire.com doesn't offer people an edit or delete option on them. Your plugin, in turn, can't change anyone else's messages.

On entire.com, your messages appear under your slug in title case, followed by "(plugin)": a plugin with the slug `deploy-previews` appears as "Deploy Previews (plugin)".

## Where your comment appears

A repository's trail is a change on a project trail. Your comment belongs to that change, so open the change on entire.com to find it. The trail's own discussions list shows only discussions on the project trail, so your comment isn't there.

## What discussions:write doesn't cover

* Resolving, reopening or retitling a discussion. These need `contents:write`.
* Starting a review, which also needs `contents:write`.
* Review comments and approvals.
* Discussions on a project trail. Only trails on a repository accept plugin comments.
* Reading discussions, which needs `contents:read`.

## Errors

These are specific to discussions. For errors when minting the token, see [Act for an installation](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#errors).

| Status and detail | Meaning |
| :- | :- |
| `403 no discussion write access to repo` | The token doesn't carry `discussions:write` for this repository. Entire gives the same answer for a repository that doesn't exist. |
| `403 no read access to repo` | You listed or read discussions without `contents:read`. |
| `403 no write access to repo` | You tried to resolve or retitle a discussion. |
| `403 automation principals cannot perform this action` | The trail belongs to a project, not a repository. |
| `404 Message not found` | The message is gone, or another installation or a person posted it. |
| `400` | The title or body is empty or too long. The `detail` names the limit. |

## With the Go SDK

Add `Discussions: plugin.Write` to `Config.Permissions`, matching your registration. `inst.RepositoryClient` then returns a client whose `Trails` methods post and edit comments.

This keeps one summary comment per trail current, from a [trail event](https://docs.entire.com/guides/api/plugins/trail-events.md#with-the-go-sdk):

```go theme={null}
func updateSummary(ctx context.Context, inst *plugin.Installation, change *plugin.TrailChange, summary string) error {
	client, repoID, err := inst.RepositoryClient(ctx, change.Repository.ID,
		plugin.Permissions{Contents: plugin.Read, Discussions: plugin.Write})
	if err != nil {
		return err
	}
	trail := int64(change.Trail.Number)
	comment, ok := loadComment(change.Trail.ID)
	if !ok {
		created, err := client.Trails.CreateDiscussion(ctx, repoID, trail,
			entire.DiscussionCreateRequest{Title: "Deploy previews", Body: summary})
		if err != nil {
			return err
		}
		return saveComment(change.Trail.ID, created.Data.ID, created.Data.Messages[0].ID)
	}
	_, err = client.Trails.UpdateMessage(ctx, repoID, trail, comment.DiscussionID, comment.MessageID,
		entire.MessageRequest{Body: summary})
	return err
}
```

`loadComment` and `saveComment` are your own functions. Key them on the trail's ID, which never changes. To find a comment you didn't store, range over `client.Trails.ListDiscussionsAll` and match `CreatedByIdentity.Automation.InstallationID` against `inst.ID()`.
