# Comments & activity

> Post and read task comments (plain text or a rich document, strictly sanitized, with mentions) and read the run's append-only activity trail.

Source: https://developers.processcart.com/guides/comments-activity

Operations: [Comments endpoints](https://developers.processcart.com/reference/comments) · [Activity endpoints](https://developers.processcart.com/reference/activity).

## Comments

A comment sits on one task of a run. Through the API a comment is authored **by the key** (`author.kind` = `api`, `name` = the key's name) and is shown in the app that way.

### Posting

`POST /v1/runs/{id}/comments` takes the task and **exactly one** of `text` or `body`:

```json
{ "taskKey": "review", "text": "Payment confirmed by the bank.\n\n@Ada Lovelace please release the order." }
```

- `text` is plain text (1–20,000 characters): blank lines separate paragraphs, a single newline is a line break, and `@Name` becomes a mention when it matches exactly one current member.
- `body` is a rich document in ProseMirror JSON (`{ "type": "doc", "content": [ … ] }`). It is sanitized **strictly**: the allowed node types are paragraph, text, heading, bulletList, orderedList, listItem, blockquote, codeBlock, hardBreak, horizontalRule, image, table, tableRow, tableCell, tableHeader, mention, mergeTag (an inline field reference, `{ "type": "mergeTag", "attrs": { "fieldKey": "<field key>" } }` — a malformed key drops the tag) and embed (a video embed whose `attrs.src` is an https URL on YouTube, Loom or Vimeo — any other source is removed); the allowed marks bold, italic, code, link, textStyle and highlight. Anything else is dropped on save, never refused. A mention is `{ "type": "mention", "attrs": { "id": "<member user id>" } }` — its label is always re-derived from the member list, and a mention of a non-member is removed.
- Mentioned members are notified as they would be in the app.
- A `taskKey` the run does not have is a 422 (`errors[].code` `unknown_task`).
- The answer is `201 Created` with an absolute `Location` and the Comment. A comment **never changes the run's revision**, so `Expected-Revision` plays no part. An archived run refuses with 409 `run_archived`, a run in Trash with 409 `run_trashed`.

### Reading

- `GET /v1/runs/{id}/comments` lists the run's live comments, oldest first, paged (`limit` up to 200). A run in Trash still answers.
- `GET /v1/runs/{id}/comments/{commentId}` returns one comment; a deleted comment is a 404.

The Comment resource:

```json
{
  "id": "c1d2…", "runId": "9b7d…", "taskKey": "review", "runTaskId": "e4f5…",
  "author": { "kind": "api", "keyId": "6a1f…", "name": "Zapier" },
  "body": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Payment confirmed by the bank." } ] } ] },
  "text": "Payment confirmed by the bank.\n\n@Ada Lovelace please release the order.",
  "mentions": ["11111111-…"],
  "createdAt": "2026-10-07T18:42:11.000Z",
  "editedAt": null,
  "url": "https://app.processcart.com/runs/9b7d…?task=review"
}
```

`author` is `{ kind: "user", userId, name }` for a member, `{ kind: "api", keyId, name }` for a key, `{ kind: "guest", email }` for a guest, or `{ kind: "unknown" }` when the author can no longer be named. `text` is the body as plain text (mentions read `@Name`).

**Editing and deleting comments are not yet available through the API** (they are in the app); see the [changelog](https://developers.processcart.com/changelog). Files attached to comments are app-only too — a key can list and download them ([files](https://developers.processcart.com/guides/tasks-approvals-files#files)).

## Activity

`GET /v1/runs/{id}/activity` is the run's append-only trail: every recorded change with who did it, the raw payload the app stores, and the human sentence the Activity tab shows.

```json
{
  "id": "a1b2…", "runId": "9b7d…",
  "type": "field_changed", "category": "fields",
  "actor": { "kind": "api", "keyId": "6a1f…", "name": "Zapier" },
  "taskKey": "details",
  "payload": { "fieldKey": "amount", "label": "Amount", "from": 1700.5, "to": 1800 },
  "description": "API key Zapier set Amount: $1,700.50 → $1,800.00",
  "createdAt": "2026-10-07T18:42:11.000Z"
}
```

- `type` is an **open** vocabulary (`run_started`, `field_changed`, `formula_recalculated`, `task_completed`, `task_uncompleted`, `approval_decided`, `approval_invalidated`, `comment_added`, `attachment_added`, `run_status_changed`, …); tolerate values you do not know. `category` files it under one of `fields`, `tasks`, `approvals`, `files`, `comments` or `run`.
- `actor` is who did it, by `kind`: `user` (a member, with `name` when known), `api` (a key: `keyId`, `name`), `guest` (`email`), `schedule` (a recurrence schedule: `scheduleId`, `name`), `rule` (an automation rule: `ruleId`, `name`), `system` for the engine's own work, and `unknown` for an author that can no longer be resolved. Like `type`, the vocabulary is open: tolerate kinds you do not know.
- `description` is rendered the way the app renders it; branch on `type` and `payload` when you need the machine fields.

Query parameters: `type` (one event type), `category`, `since` (strictly after an instant), `order` (`desc` — newest first — by default; `asc` reads forward), `limit` (up to 200) and `cursor`. Events written in one transaction share a `createdAt`; the cursor orders them by id as well, so none is skipped or repeated across a page boundary. To **tail** a run, keep the last page's cursor with `order=asc` and ask again — `since` is the coarse filter, the cursor the exact one. A run in Trash still answers.
