# Safe writes and retries

Protect updates with the version you read and recover uncertain creations with the same request, key and replay scope.

Last updated 8 November 2026

Keep the state that makes each write safe to repeat. For a file or task update, keep the hash or version you read so the update cannot silently overwrite intervening work. For a creation that supports replay, keep its original request and idempotency key so a retry can recover the result without duplicating the action.

Send the JSON bodies below as `POST` requests with `Authorization: Bearer <token>` and `Content-Type: application/json`. Replace `{workspaceId}` in these endpoints with your workspace ID:

| Operation | Endpoint |
| --- | --- |
| Read | `https://app.cabane.ai/api/workspaces/{workspaceId}/ops/query` |
| Write | `https://app.cabane.ai/api/workspaces/{workspaceId}/ops/mutate` |

## Protect a file update

`files.write` creates a file by default. If the path already exists, it returns an error with kind `conflict` and code `name_conflict`. Setting `overwrite: true` replaces the entire content; pair it with `ifSha` to check that the content is still the version you read.

First, send `files.read` to the query endpoint:

```json
{
  "op": "files.read",
  "args": {
    "path": "/api-example/hello.md"
  }
}
```

Keep `result.content` and `result.sha256`. Make your changes to the content you read, then send `files.write` to the mutate endpoint with the returned hash as `ifSha`.

The unchanged sample file from [First API request](https://cabane.ai/docs/first-api-request) contains `# Hello\n\nCreated from the Cabane API.\n`. For that exact content, the guarded update is:

```json
{
  "op": "files.write",
  "args": {
    "path": "/api-example/hello.md",
    "content": "# Hello\n\nUpdated from the Cabane API.\n",
    "overwrite": true,
    "ifSha": "edb3a4c9af5d8b0e6e755719bae5ffeee15c6aaca2bb994ec08f678dc116f52e"
  }
}
```

Use the hash from your own read when the file has different content. `files.edit` also accepts `ifSha`.

If the content changed after your read, the write returns kind `conflict`, code `content_conflict` and the current hash. Read the file again, reconcile your edits with its current content, then submit the revised content with the new hash. Substituting the current hash into your old request without reviewing the content can overwrite someone else's work.

## Protect a task update

Read a task before changing it. Send this body to the query endpoint, replacing the example UUID with your task's ID:

```json
{
  "op": "tasks.read",
  "args": {
    "id": "2c8c7a74-e43b-4d51-bafd-906318c982e0"
  }
}
```

Keep `result.task.version`. If the returned version is `7`, send this update to the mutate endpoint:

```json
{
  "op": "tasks.update",
  "args": {
    "id": "2c8c7a74-e43b-4d51-bafd-906318c982e0",
    "ifVersion": 7,
    "title": "Publish the reviewed weekly report"
  }
}
```

Both the ID and version are examples: use your task's ID and the version returned by your read. A stale version produces a conflict. Read the task again, reconcile your intended change with its current state, and reapply it with the new version. The [API reference](https://cabane.ai/docs/api) lists the version requirements for each task write.

## Keep the replay scope with the key

An `idempotencyKey` identifies one intended action within the operation's scope. Save the key and original request in durable integration state before sending them. Keep enough context to retry in the same workspace, room or conversation, and as the same author when posting a message.

| Operation | Scope of one key | Replay behavior |
| --- | --- | --- |
| `conversations.post` | One author, a person or agent, in one conversation | Returns the original result with `replayed: true`; ignores a changed body. |
| `conversations.create` | One room | Returns the original result with `replayed: true`; ignores a changed body. |
| `tasks.create` | One workspace across all its boards | Replays only when the arguments are exactly the same. Changed arguments return kind `conflict` and code `idempotency_conflict`. |

### Retry a message as the same author

`conversations.post` accepts an idempotency key of 1–255 characters. Send this body to the mutate endpoint, replacing the example UUID with your conversation ID:

```json
{
  "op": "conversations.post",
  "args": {
    "id": "e0c7a639-8354-4a7c-98ae-7fd6ecf18249",
    "body": "The weekly report is ready.",
    "idempotencyKey": "weekly-report-2026-11-08"
  }
}
```

Retry with the same key, conversation and author. The replay returns the original result without another message, notification or agent call. Another person or agent using that key in the same conversation can create a separate message. Changing tokens while acting as the same person does not by itself change the author.

### Retry a conversation creation

For `conversations.create`, keep the same room and key. A replay creates no second conversation or agent call. A concurrent retry with the same room and key waits for and returns the original result.

### Give each new task its own key

`tasks.create` requires an idempotency key. Send this body to the mutate endpoint, replacing the example UUID with your board ID:

```json
{
  "op": "tasks.create",
  "args": {
    "boardId": "6a97f682-2fd2-4896-a425-d004c94830bb",
    "title": "Publish the weekly report",
    "body": "Prepare the report for the team.",
    "idempotencyKey": "weekly-report-task-2026-11-08"
  }
}
```

Retry this creation with exactly the same arguments. The key belongs to the workspace across all its boards: reusing it with a different `boardId` returns `idempotency_conflict`. Give every distinct task creation a new key, including tasks on different boards. Use a task update operation to change a task you already created.

## Recover an unknown result

A timeout or lost connection does not establish that a write failed. Recover according to the operation:

- For `conversations.post`, `conversations.create` and `tasks.create`, resend the original arguments and saved key in the original scope. A message retry must retain the original person or agent as its author. Keep the same key when the result is unknown; a fresh key identifies a new action.
- For other writes, read the target to establish what landed before deciding what work remains. If an update is still needed, reconcile it with the current state and use the hash or version from that read where the operation supports it.

When a response arrives, check its `ok` field even if the HTTP status is 200. Success has `ok: true` and a `result`; an operation failure has `ok: false` and an `error` containing `kind`, `code`, `message` and sometimes `current`. An explicitly transient operation error permits retrying the request as sent. For other errors, follow the specific recovery in [Requests and responses](https://cabane.ai/docs/requests-and-responses).
