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

# Writes and the action ledger

> Record notes, tasks and status changes against system-of-record entities, retry them safely, and read them back.

Records writes are governed commands, such as "add a note to this record" or "set this record's status". Nexio records each command in the action ledger: an append-only record of every command your people issued, kept for your organization. Current notes, tasks and statuses are projections of that ledger, and you read them back through their own routes.

<Warning>
  Nothing is written to the connected system of record today. A note or task you record appears in Nexio's reads and in applications built on them. It does not appear in the source system. Routes that would change the source system refuse with 403 `overlay_read_only` (see [What is refused](#what-is-refused)).
</Warning>

## How it works

1. Your server sends `POST /api/v1/records/actions` with the person the commands are for (`actor_principal`) and 1 to 20 commands.
2. When a command addresses a record of the system of record, Nexio resolves that person's authority as it does for a read (see [Authority and scope](/data-services/scope)), and each such command must address a record the person may open. A note on a Catalog entity needs no system-of-record authority: a batch whose commands all address Catalog entities resolves no connection, and its response carries no `serving` block.
3. Nexio applies the whole batch in one transaction. If any command fails, none is recorded.
4. Each command gets a sequence number (`seq`) in the organization's ledger. The response returns them with `action_seq`, the ledger's latest sequence number.
5. You read the current state back from `/notes`, `/tasks` or `/workflows`, or follow the ledger itself from `/actions`. These three reads return the state of system-of-record records for one connection; a note on a Catalog entity appears only in the ledger read (`/actions`).

## Commands

The command set is fixed in code: 14 commands, and no organization adds its own. Ten of them are the commands you send:

| Command                                       | Targets                                        | Payload                                                                                                                            |
| --------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `note.create`                                 | a system-of-record record, or a Catalog entity | `{"body": "..."}`, not empty                                                                                                       |
| `note.edit`                                   | same as the note                               | `{"note_id": "<uuid>", "body": "..."}`                                                                                             |
| `note.delete`                                 | same as the note                               | `{"note_id": "<uuid>"}`                                                                                                            |
| `task.create`                                 | a system-of-record record                      | `{"title": "...", "due": "YYYY-MM-DD", "assignee_principal": "..."}`. `due` and `assignee_principal` are optional.                 |
| `task.edit`                                   | same as the task                               | `{"task_id": "<uuid>", "title": "...", "due": "YYYY-MM-DD"}`. Send at least one of `title` and `due`. `"due": ""` clears the date. |
| `task.assign`                                 | same as the task                               | `{"task_id": "<uuid>", "assignee_principal": "..."}`                                                                               |
| `task.complete`, `task.reopen`, `task.delete` | same as the task                               | `{"task_id": "<uuid>"}`                                                                                                            |
| `status.set`                                  | a system-of-record record, per field           | `{"field": "...", "value": "...", "reason": "..."}`. `reason` is optional, at most 2,000 characters.                               |

The other four are not yours to send: `source_activity.create` and `renewal_decision.set` are refused with 403 `overlay_read_only` ([What is refused](#what-is-refused)), and `book_edit_intent.transition` and `book_edit_intent.cancel` are internal, so a batch that names one answers 400 `action_schema_unknown`.

Every command carries `command`, `schema_rev` (always `1` today), exactly one target, a `payload`, and an `idempotency_key`. A target is a `client_key` or a `policy_key`, the opaque keys the system of record's rows carry, or, for notes only, a Catalog entity (`catalog_entity_type` plus `catalog_id`).

Payloads are validated strictly: an unknown field is refused. A command's optional `id` must be a UUID. When you omit it, Nexio generates one, and for `note.create` and `task.create` that id becomes the note or task id.

`status.set` writes one registered workflow field on one record. The fields, the record kind each applies to, and their allowed values are fixed in code. An unknown field, or a value the field does not allow, answers 400 `action_payload_invalid`.

## Idempotency and safe retries

* Every command needs an `idempotency_key`, a UUID you generate. It is unique across your organization's ledger.
* Sending the same key again with the same command, target, payload, actor and basis records nothing new. The response returns the original `command_id` and `seq` with `"idempotent_replay": true`, and the status is still 200.
* Sending the same key with anything different answers 400 `action_payload_invalid`.
* So a timed-out batch is safe to resend unchanged. Generate new keys only for new intent.

The optional `basis` names the ledger position the person was looking at (`seq`, from a previous `action_seq`). A stale basis does not cause a refusal. When another command changed the same note, task or status after that position, the projection marks the row `applied_over_concurrent: true` so your application can tell the person.

## Scope-bounded writes

* `actor_principal` is the person the commands are for. `actor_type` is `producer` (a person) or `agent` (an AI agent acting for that person).
* If you send `X-Nexio-Acting-Principal`, it must equal `actor_principal`, or the request answers 400 `invalid_request`. If you omit it, Nexio resolves authority for `actor_principal`.
* A command addressing a record outside that person's authority answers 403 `action_out_of_scope`, and the whole batch is refused.
* A session with `X-Nexio-Records-Lens` cannot write: 403 `book_lens_read_only`.

## Example: record a note and a task

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.usenexio.com/api/v1/records/actions \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "X-Nexio-Acting-Principal: user_01J8Z3K4M5N6P7Q8R9S0T1U2V3" \
    -H "X-Nexio-Acting-Email: dana.ortiz@harborgroup.example" \
    -d '{
      "actor_principal": "user_01J8Z3K4M5N6P7Q8R9S0T1U2V3",
      "actor_type": "producer",
      "commands": [
        {
          "command": "note.create",
          "schema_rev": 1,
          "client_key": "bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg",
          "payload": { "body": "Supplier asked to move the review meeting to October." },
          "idempotency_key": "5b1e3c7a-2f4d-4a8e-9c1b-0d2e3f4a5b6c"
        },
        {
          "command": "task.create",
          "schema_rev": 1,
          "client_key": "bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg",
          "payload": { "title": "Request the signed security questionnaire", "due": "2026-10-01" },
          "idempotency_key": "a7d9e2f1-4c3b-4a5d-8e6f-7a8b9c0d1e2f"
        }
      ]
    }'
  ```

  ```python Python theme={null}
  import os
  import requests

  RECORD = "bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg"
  PRINCIPAL = "user_01J8Z3K4M5N6P7Q8R9S0T1U2V3"

  response = requests.post(
      "https://api.usenexio.com/api/v1/records/actions",
      headers={
          "Authorization": "Bearer " + os.environ["NEXIO_API_KEY"],
          "X-Nexio-Acting-Principal": PRINCIPAL,
          "X-Nexio-Acting-Email": "dana.ortiz@harborgroup.example",
      },
      json={
          "actor_principal": PRINCIPAL,
          "actor_type": "producer",
          "commands": [
              {
                  "command": "note.create",
                  "schema_rev": 1,
                  "client_key": RECORD,
                  "payload": {"body": "Supplier asked to move the review meeting to October."},
                  "idempotency_key": "5b1e3c7a-2f4d-4a8e-9c1b-0d2e3f4a5b6c",
              },
              {
                  "command": "task.create",
                  "schema_rev": 1,
                  "client_key": RECORD,
                  "payload": {"title": "Request the signed security questionnaire", "due": "2026-10-01"},
                  "idempotency_key": "a7d9e2f1-4c3b-4a5d-8e6f-7a8b9c0d1e2f",
              },
          ],
      },
      timeout=30,
  )
  response.raise_for_status()
  print(response.json()["results"])
  ```

  ```typescript TypeScript theme={null}
  const RECORD = "bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg";
  const PRINCIPAL = "user_01J8Z3K4M5N6P7Q8R9S0T1U2V3";

  const response = await fetch("https://api.usenexio.com/api/v1/records/actions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
      "Content-Type": "application/json",
      "X-Nexio-Acting-Principal": PRINCIPAL,
      "X-Nexio-Acting-Email": "dana.ortiz@harborgroup.example",
    },
    body: JSON.stringify({
      actor_principal: PRINCIPAL,
      actor_type: "producer",
      commands: [
        {
          command: "note.create",
          schema_rev: 1,
          client_key: RECORD,
          payload: { body: "Supplier asked to move the review meeting to October." },
          idempotency_key: "5b1e3c7a-2f4d-4a8e-9c1b-0d2e3f4a5b6c",
        },
        {
          command: "task.create",
          schema_rev: 1,
          client_key: RECORD,
          payload: { title: "Request the signed security questionnaire", due: "2026-10-01" },
          idempotency_key: "a7d9e2f1-4c3b-4a5d-8e6f-7a8b9c0d1e2f",
        },
      ],
    }),
  });
  if (!response.ok) throw new Error(`append failed: ${response.status}`);
  console.log((await response.json()).results);
  ```
</CodeGroup>

```json 200 OK theme={null}
{
  "duration_ms": 84,
  "results": [
    { "command_id": "3f6c2d1e-8b7a-4c5d-9e0f-1a2b3c4d5e6f", "seq": 4127 },
    { "command_id": "9d8e7f6a-5b4c-4d3e-8f2a-1b0c9d8e7f6a", "seq": 4128 }
  ],
  "action_seq": 4128,
  "serving": {
    "binding_id": "2c4e6a8b-0d1f-4a3c-9e5b-7d9f1a3c5e7b",
    "overlay_rev": 0,
    "as_of": "0001-01-01T00:00:00Z",
    "source": {
      "mode": "query_first",
      "current": true,
      "fetched_at": "0001-01-01T00:00:00Z"
    }
  },
  "scope": { "kind": "Self", "selection": "boundary", "selection_source": "none" }
}
```

Sending the same request again returns the same `command_id` and `seq` values, each with `"idempotent_replay": true`. On the action routes `serving.as_of` and `serving.source.fetched_at` are the zero time, even when the scope check for a command that addresses a system-of-record record queries the warehouse.

## Read it back

| Route                                                                      | Returns                                                                                                                                          | Paging                                                                                                                                                                           |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`GET /api/v1/records/notes`](/api-reference/data-services/list-notes)     | Current notes. Pass `client_key` or `policy_key` (not both) to narrow to one account or policy; without either, the notes in the person's scope. | None. Oldest first, up to 10,000 rows; more answers 400 `action_list_too_large`.                                                                                                 |
| [`GET /api/v1/records/tasks`](/api-reference/data-services/list-tasks)     | Current tasks, with `status` `open` or `completed`.                                                                                              | Same as notes.                                                                                                                                                                   |
| `GET /api/v1/records/workflows`                                            | Current `status.set` values. Optional `field` filter.                                                                                            | Same as notes.                                                                                                                                                                   |
| [`GET /api/v1/records/actions`](/api-reference/data-services/list-actions) | The ledger itself, command by command. Optional `family` filter (for example `note`).                                                            | `since_seq` and `limit` (default 100; values above 500 are treated as 500). Pass `next_seq` back as `since_seq`. `next_seq` is null when a page holds fewer rows than the limit. |

Deleted notes and tasks are hidden from the projections. A row about a `policy` record identifies it by the source's tenant key and record id (`ams360_datasource` and `policy_id`) rather than by `policy_key`. A person whose scope is `Self` sees only ledger rows they authored on `/actions`; an `All` or `Platform` scope sees the whole ledger.

<CodeGroup>
  ```bash curl theme={null}
  curl "https://api.usenexio.com/api/v1/records/tasks?client_key=bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg" \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "X-Nexio-Acting-Principal: user_01J8Z3K4M5N6P7Q8R9S0T1U2V3" \
    -H "X-Nexio-Acting-Email: dana.ortiz@harborgroup.example"
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.get(
      "https://api.usenexio.com/api/v1/records/tasks",
      params={"client_key": "bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg"},
      headers={
          "Authorization": "Bearer " + os.environ["NEXIO_API_KEY"],
          "X-Nexio-Acting-Principal": "user_01J8Z3K4M5N6P7Q8R9S0T1U2V3",
          "X-Nexio-Acting-Email": "dana.ortiz@harborgroup.example",
      },
      timeout=30,
  )
  response.raise_for_status()
  for task in response.json()["data"]:
      print(task["title"], task["status"])
  ```

  ```typescript TypeScript theme={null}
  const url = new URL("https://api.usenexio.com/api/v1/records/tasks");
  url.searchParams.set("client_key", "bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg");
  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
      "X-Nexio-Acting-Principal": "user_01J8Z3K4M5N6P7Q8R9S0T1U2V3",
      "X-Nexio-Acting-Email": "dana.ortiz@harborgroup.example",
    },
  });
  if (!response.ok) throw new Error(`tasks read failed: ${response.status}`);
  for (const task of (await response.json()).data) console.log(task.title, task.status);
  ```
</CodeGroup>

```json 200 OK theme={null}
{
  "duration_ms": 41,
  "data": [
    {
      "id": "9d8e7f6a-5b4c-4d3e-8f2a-1b0c9d8e7f6a",
      "entity_type": "client",
      "client_key": "bk1_SEFSQk9SHzBlOWQ4YzdiLTZhNWYtNGUzZC0yYzFiLTBhOWY4ZTdkNmM1Yg",
      "author_principal": "user_01J8Z3K4M5N6P7Q8R9S0T1U2V3",
      "title": "Request the signed security questionnaire",
      "due": "2026-10-01T00:00:00Z",
      "status": "open",
      "attach_status": "attached",
      "applied_seq": 4128
    }
  ],
  "action_seq": 4128,
  "serving": {
    "binding_id": "2c4e6a8b-0d1f-4a3c-9e5b-7d9f1a3c5e7b",
    "overlay_rev": 0,
    "as_of": "0001-01-01T00:00:00Z",
    "source": {
      "mode": "query_first",
      "current": true,
      "fetched_at": "0001-01-01T00:00:00Z"
    }
  },
  "scope": { "kind": "Self", "selection": "boundary", "selection_source": "none" }
}
```

`attach_status` is `attached` on every row written today.

## What is refused

These write paths exist and always refuse, because they would change the system of record:

| Request                                                                         | Answer                                                                                                                                                               |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A batch containing `source_activity.create` or `renewal_decision.set`           | 403 `overlay_read_only`. The body, `actor_principal` and `actor_type` checks run first; then the whole batch is refused before any command is validated or recorded. |
| `PUT /api/v1/records/accounts/{clientKey}/policies/{policyKey}/coverage-record` | 403 `overlay_read_only`                                                                                                                                              |
| `POST /api/v1/records/edit-intents/{id}/cancel`                                 | 403 `overlay_read_only`                                                                                                                                              |

## Limits

| Limit                    | Value                                            |
| ------------------------ | ------------------------------------------------ |
| Commands per batch       | 1 to 20                                          |
| Payload per command      | 32 KiB                                           |
| Request body             | 1 MiB                                            |
| `status.set` reason      | 2,000 characters (Unicode characters, not bytes) |
| Projection rows per read | 10,000                                           |
| Ledger page (`/actions`) | default 100, maximum 500                         |

## Errors

| Status | Code                      | Cause                                                                                                                                                                                                                                                                                   | Fix                                               |
| ------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| 400    | `invalid_request`         | A body that is not one JSON value or is over 1 MiB, missing `actor_principal`, `actor_type` not `producer` or `agent`, no commands, more than 20 commands, a command with no target, two targets or a malformed key, or an acting-principal header that differs from `actor_principal`. | Fix the body.                                     |
| 400    | `action_schema_unknown`   | Unknown command, or a `schema_rev` other than `1`.                                                                                                                                                                                                                                      | Use a command from the table.                     |
| 400    | `action_payload_invalid`  | Payload fails validation or is over 32 KiB, a target the command does not accept, a command `id` that is not a UUID, missing or non-UUID `idempotency_key`, an idempotency key reused for a different command, an unknown or deleted note or task, or a create whose id already exists. | Fix the payload, or use a new key for new intent. |
| 400    | `action_list_too_large`   | A projection read matched more than 10,000 rows.                                                                                                                                                                                                                                        | Narrow to one account or policy.                  |
| 403    | `action_out_of_scope`     | The target is outside the person's authority.                                                                                                                                                                                                                                           | Check the person's access.                        |
| 403    | `overlay_read_only`       | A command or route that would change the system of record.                                                                                                                                                                                                                              | Not available today.                              |
| 403    | `book_lens_read_only`     | The request carried `X-Nexio-Records-Lens`.                                                                                                                                                                                                                                             | Drop the lens for writes.                         |
| 403    | `insufficient_capability` | A scoped key without `actions:write` (writes) or `actions:read` (reads).                                                                                                                                                                                                                | Ask Nexio to add the capability.                  |
| 403    | `scoped_key_required`     | A sandbox or test organization key.                                                                                                                                                                                                                                                     | Use the live key or a scoped key.                 |
| 500    | `audit_write_failed`      | The audit record for the write could not be written, so nothing was recorded.                                                                                                                                                                                                           | Retry.                                            |

Authority refusals (`identity_unmapped`, `scope_unavailable` and the rest) are listed on [Authority and scope](/data-services/scope#refusal-codes). Read-time failures (`book_unavailable`) are on [Completeness and errors](/data-services/completeness).

## Next

<CardGroup cols={2}>
  <Card title="Append commands" href="/api-reference/data-services/create-actions">Endpoint reference.</Card>
  <Card title="Authority and scope" href="/data-services/scope">How the person's authority is resolved.</Card>
</CardGroup>
