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

# Safe writes and approvals

> Which Coffee Chats writes apply at once and which wait for your approval, how idempotency keys work, and the result object every write returns.

Thirty-one of the 85 tools change something. Twenty-one of them write as soon as the agent calls them. Nine of them write nothing until you approve the change, in your browser or, for a contact update in Claude or ChatGPT, in an approval card beside the agent's reply. One starts work in the Coffee Chats extension in your browser. After reading this page you will know which changes fall on which side, why the line is drawn there, what every write result looks like, and what happens between an agent's proposal and the moment something is written.

## The line between the two

A change is a safe write when it affects only your own account and you can set it back yourself from a screen in the app. A change is consequential when it alters what other people see, what they can book, or what reaches them by email.

| | Safe write | Needs your approval |
| - | - | - |
| How many tools | 17 | 9 |
| Risk class in the catalogue | `safe_write` | `consequential` (nine of its thirteen tools) |
| When it is written | As soon as the agent calls the tool | Only after you approve it, in a browser or, for a contact update, in the approval card |
| What it touches | Your own settings and your own records | Your public booking page, its event types, one thank-you email, up to 25 of your contacts at once, research on up to 25 contacts, which spends credits, and the settings your outreach emails are written with |
| How you undo it | Change it back in the app | Change it back in the app, except for a message that has already been sent or research that has already started |

The seventeen safe writes cover your profile preferences, your job-search stage, adding contacts, importing contacts and jobs from a spreadsheet, the notes and tags on a contact, the notes and outcome on a meeting, a campaign's name, description and goal, a campaign's sending state, adding and editing internship applications, an internship application's stage and priority, one part of a ready resume or cover letter, and the withdrawal of a review request you have not decided yet.

Five of the nine that need your approval are about your booking page: its text, whether it is public, its scheduling rules, the details of one event type, and whether one event type is archived. The sixth sends a thank-you you already drafted, to the address stored on the contact linked to that meeting, at a time in the next forty-eight hours. The seventh, `contacts.update`, changes the details, status, rating, tags or notes of up to 25 of your contacts at once. It touches only your own records, but a status change can cancel a contact's scheduled follow-ups or let automated sends reach that contact again, and one call can change many contacts, so it waits for you. The eighth, `contacts.research.start`, starts research on up to 25 of your contacts. Research spends credits, so it waits for you and is approved only in a browser, never in the approval card. The ninth, `mail.outreach.update`, changes your preferred name, email signature, scheduling link and email tone. Those go into every outreach email you send, so it waits for you. `mail.drafts.send` is one of the twenty-one that write immediately: it sends one outreach draft you already stored, and it cannot change the subject, the body, or the recipient. `mail.batch.enqueue` writes immediately too: it queues a table of your own outreach drafts, after each row is checked. `documents.resumes.generate` and `documents.jobs.tailor` write immediately too: they write a resume, or a tailored resume and cover letter, and use credits as soon as the agent calls them, so they need their own permission, `documents:generate`.

## Why an idempotency key

Eighteen of the nineteen write tools described here take an `idempotencyKey`, a string of 16 to 128 characters that the agent chooses for one particular change. It is how Coffee Chats recognises a repeat of a call it has already seen.

* Send the same tool, the same key and the same input again, and Coffee Chats returns the answer it already computed. Nothing is written twice. That is what makes a retry safe after a lost answer.
* Send the same key with different input, and the call is refused with `VERSION_CONFLICT`. A key names one change, and it cannot be pointed at a different one.
* A genuinely new change needs a new key. Reusing an old key to force a retry returns the old answer.

A stored change is made unique by the connection, the tool name and the key together, so the same key sent to two different tools names two different changes and neither can overwrite the other.

The nineteenth write tool is `actions.cancel`, which withdraws a review request. Its whole input is `operationId`, and it takes no idempotency key at all, because the identifier of the review request it withdraws already names one thing. Sending it a key is refused before the tool runs.

`contacts.create` and `contacts.bulk_create` are not among the nineteen. They take no key: a repeated call finds the contacts the first call added and reports each row as a duplicate with that contact's id, so nobody is added twice. They answer with one outcome per row (`created`, `duplicate` or `error`) rather than the result object below.

`jobs.applications.create` and `jobs.applications.update` are not among the nineteen either, and take no key. A repeated create finds the application the first call added and returns it with `duplicate: true`, and an update sets each field it names to the value sent, so sending it again changes nothing more. Both answer with the application's id, its stage and whether the Coffee Chats extension can apply to it now, rather than the result object below.

`contacts.import` and `jobs.applications.import` are not among the nineteen either, and take no key. A repeated import finds the rows the first call added and counts them as skipped, so nothing is added twice. They answer with counts of the rows created, updated, skipped and failed and the first ten row errors, rather than the result object below.

The three document writes, `documents.resumes.generate`, `documents.jobs.tailor` and `documents.section.update`, are not among the nineteen either, and take no key. A repeated generate or tailor returns the ready document without a new charge unless the agent asks to regenerate it, and each section edit saves a new version. They answer with the document itself rather than the result object below.

`mail.drafts.send`, `mail.batch.enqueue` and `jobs.applications.apply` are not among the nineteen either, and take no key. Each one's page in the [tool reference](/mcp/tools/overview.md) prints the result it answers with.

## The result object

Every write tool answers with one structured object rather than free text. This section is the one description of that object, and every example on every page matches it.

The object arrives under `structuredContent` in the tool response. The same JSON is repeated as text in the response's `content` array, so an agent host that reads only text still sees it. A completed safe write looks like this in full:

```json Completed write result theme={"system"}
{
  "structuredContent": {
    "tag": "completed",
    "scenario": "success",
    "operationId": "0123456789abcdef01234567",
    "operationStatus": "succeeded",
    "result": { "summary": "Contact notes updated", "itemCount": 1 }
  },
  "content": [{ "type": "text", "text": "the same object, as a JSON string" }]
}
```

The examples on the rest of these pages print the inner object on its own, because that is the part an agent branches on. Every one of them sits under `structuredContent` on the wire.

### Read `tag` first, then `status`

A write result carries either a `tag` field or a `status` field. It never carries both. Branch on `tag` when it is there, and on `status` when it is not.

| Discriminator | Values a write tool sends | What that group means |
| - | - | - |
| `tag` | `requires_confirmation`, `accepted`, `completed` | The call is still going somewhere, or it finished by writing. |
| `status` | `failed`, `cancelled`, `uncertain` | The call is over and nothing was written, or nothing can be proved. |

Two contracts meet here, which is why there are two field names. The result format is the union of the contract for a tool call and the contract for reading the state of one change. The first is keyed on `tag`, the second on `status`, and a write tool answers on whichever of the two fits the state it is reporting.

### Every field

| Field | Type | When it is present | What it means |
| - | - | - | - |
| `tag` | string | On a `requires_confirmation`, `accepted` or `completed` result | The outcome group. Branch on this first. |
| `status` | string | On a `failed`, `cancelled` or `uncertain` result | The outcome group, when there is no `tag`. |
| `scenario` | string | Always | A narrower label for the same outcome. It is informational. |
| `operationId` | string | Always | Names this one change. |
| `operationStatus` | string | With `tag` of `accepted` or `completed` | `pending` or `processing` beside `accepted`, and `succeeded` beside `completed`. |
| `actionStatus` | string | With `tag` of `requires_confirmation` | Always `pending_approval`. |
| `handoff` | object | With `tag` of `requires_confirmation` | `kind` is `action_review` and `url` is the review link. |
| `nextStep` | string | With `tag` of `requires_confirmation`, from `contacts.update`, `contacts.update.get` and `contacts.research.start` | One sentence for the agent: where the student approves, the review link, and the tool to poll. |
| `preview` | object | With `tag` of `requires_confirmation`, on the answer `contacts.update` or `contacts.research.start` gives when it proposes | What the approval card shows: the summary, each contact's changes, the contacts left out with the reason, any follow-up warning, the expiry, and whether the card may offer Approve. For research it shows each contact with its cost, the total cost and the available balance instead of changes. |
| `result` | object | With `tag` of `completed`, and on a contact update with `status` of `failed` that had already recorded contacts | `summary`, a sentence of at most 240 characters, and `itemCount`, a whole number. The written value itself is never sent back. A contact update also carries `applied`, the ids of the contacts it updated, and `skipped`, each with a `contactId` and a `reason`. A failed contact update carries only `applied` and `skipped`, so the agent can tell which contacts changed before it stopped. A research batch carries the same two lists, with `applied` naming the contacts whose research started, plus `chargeableEnqueued`, the number of them that may spend a credit. |
| `error` | object | With `status` of `failed`, `cancelled` or `uncertain` | `code`, `retryable` and `correlationId`. |

`scenario` is one of eight values on a write result: `success`, `loading`, `conflict`, `denied`, `expired`, `unavailable`, `cancelled` and `uncertain`. The first two appear on results that have not failed. The other six appear on the three groups that `status` reports. The contract also declares `empty`, and only a contact update sends it: a completed contact update that changed no contact, because every contact it named had changed or been deleted since you were shown it.

### The two identifiers

`operationId` names one change. On a consequential tool it is also the review request identifier, and it is the last part of the review link the tool hands back. To learn what you decided, the agent calls the same tool again and sends that value in the `operationId` field. There is no other field and no other address that carries it.

`correlationId` sits inside the `error` object and identifies the same change in the Coffee Chats logs. On every result the connector produces today it carries the same value as `operationId`. Record it, and include it when reporting a problem to support. It means nothing else to your agent.

Every Coffee Chats identifier is a 24-character hexadecimal string. The examples in these pages use a different one for each kind of record, so `c0ffeec0ffeec0ffee000001` is always a contact and `0123456789abcdef01234567` is always an operation or a review request.

### Shapes the contract allows and no tool sends

The result format is shared by tools that do not exist yet, so it declares more than any tool returns today. The `requires_user_action` tag and the `pending`, `processing` and `succeeded` values of `status` are declared and no tool an agent calls sends them. The approval card's own tool, `actions.decide`, answers with `requires_user_action` when the card cannot decide a review request, and the card shows the review link. The `requires_confirmation` shape also declares an optional `actionId` field, and no tool sends it either, so `operationId` is the only identifier a review request ever arrives under. Do not build against a result shape you have not seen documented for a specific tool. Each tool's page in the [tool reference](/mcp/tools/overview.md) lists the shapes that tool can return. Report anything else as an unrecognised answer instead of acting on it.

## What each outcome means

| Group | Field and value | What it means | What to do |
| - | - | - | - |
| Waiting for you | `tag` is `requires_confirmation` | A review request exists and nothing has been written. | Give the student the link in `handoff.url` and poll. |
| Accepted | `tag` is `accepted` | The work is under way. Nothing has been written yet. | Poll again. |
| Completed | `tag` is `completed` | The change was written, exactly once. | Stop. Read the value back if you need to show it. |
| Failed | `status` is `failed` | Nothing was written. `error.code` says why. | Follow the action in [error codes](/mcp/errors.md). |
| Cancelled | `status` is `cancelled` | The review request was withdrawn before the change started, so nothing was written. | Stop. |
| Uncertain | `status` is `uncertain` | Coffee Chats cannot prove whether the change landed, and says so rather than guessing. | Read the stored value. [Settle an uncertain result](/mcp/guides/settle-an-uncertain-result.md) is the procedure. |

An accepted result and an uncertain result are easy to confuse. Accepted means the work is still running, so poll again. Uncertain means the work is over and Coffee Chats cannot say what it did, so polling will never resolve it.

## How to poll

To poll a safe write, call the same tool again with the same idempotency key and the same input. The answer is the current state of that change. Nothing is written a second time.

To poll a tool that waits for approval, call the same tool again and send only `operationId`, set to the value the first call returned. That form of the call takes no other property. `contacts.update` is the exception: poll it with `contacts.update.get` and the same `operationId`, because an application that shows the approval card would show a new card on every call to `contacts.update`. `contacts.research.start` takes no `operationId`: to learn how a research batch ended, call it again with the same contacts and the same idempotency key, and read each contact's research with `contacts.research.get`.

Every other consequential tool publishes one input object that names the properties of both forms, the one that proposes the change and the one that asks what happened to it, and every one of them is optional in the published schema. That is what an agent host requires, and it is not a licence to mix the two. Sending `operationId` beside the fields of a proposal is refused, and so is a proposal with a property from no form at all. Each tool's page in the [tool reference](/mcp/tools/overview.md) writes out the forms that tool accepts.

In both cases, stop when the result is `completed`, `failed`, `cancelled` or `uncertain`, which are the four states nothing moves out of.

> [!WARNING]
>
> Poll at a human pace. The student has to open a link, read a page and press a button. The connector allows 120 agent calls a minute on one connection, and a tight polling loop spends that budget on one waiting change and locks the agent out of its own reads.

## The flow of one approval

A tool that waits for approval never writes on its first call. It creates a review request, hands back a link, and waits. The agent finds out what happened by calling the same tool again with `operationId`, or `contacts.update.get` for a contact update, or `contacts.research.start` with the same idempotency key for research. An agent can never approve its own review request.

```mermaid theme={"system"}
sequenceDiagram
    participant A as Agent
    participant C as Coffee Chats connector
    participant S as You, in a browser
    A->>C: Call the tool with the proposed values
    C-->>A: requires_confirmation, an operationId and a review link
    A->>S: Hand over the review link
    S->>C: Open the link, and sign in if no session is open
    C-->>S: Show the proposed values, the effect and the expiry
    S->>C: Approve or decline, or save an edited version where a form exists
    C-->>A: accepted while the approved change is pending or processing
    A->>C: Call the same tool again with the operationId
    C-->>A: completed once the change is written
```

An existing Coffee Chats session skips the sign-in step. An edit form exists only on booking page text, on scheduling rules and on event type details. The visibility and archive reviews have no edit form, because each of those carries a value with only two states.

A review request waits thirty minutes from the moment it is made. After that it expires and nothing is written. Editing the values does not extend that deadline: an edit creates a new version of the same review request, under the same identifier, and that version still needs approving.

For a contact update in Claude or ChatGPT, the approval card can stand in for the browser steps: you read the same changes beside the agent's reply and press Approve or Deny there, and the review link stays on the card if you would rather use the page. [Approvals](/mcp/approvals.md) describes the card, what the review page shows you and how each review request can end.

## The retry hint

A write can be accepted and still ask the agent to come back later. When that happens the answer is an accepted result with a hint beside it, under `_meta` rather than inside the result. The hint holds a code, the fixed value `true` for `retryable`, and a delay in milliseconds. Nothing has gone wrong and nothing has been written yet. The two codes a hint can carry are `PERMIT_BACKPRESSURE` and `PERMIT_CONTENTION`, and both mean the same thing to a caller: wait for the stated delay and call again with the same idempotency key.

The delay is at least 1 millisecond and at most 5,000, and every write tool that issues one sets it to 1,000.

> [!WARNING]
>
> Do not treat a retry hint as a failure. The change has been accepted and will proceed. If your agent stops here, it reports a failure for work that was going to succeed.

## Next steps

> **[Approvals](/mcp/approvals.md)**
>
> What the review page shows, and the nine states a review request can be in.

> **[Update a preference safely](/mcp/guides/update-a-preference.md)**
>
> One safe write worked through, with its replay and its conflict.

> **[Propose a booking page change](/mcp/guides/propose-a-booking-page-change.md)**
>
> The agent's side of one approval, end to end.

> **[Error codes](/mcp/errors.md)**
>
> Every code a write can return, its cause and the action to take.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.