> ## 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.

# Update a preference safely

> Make one safe write, replay it after a lost answer, recognise the conflict a reused key causes, and handle the retry hint.

This guide shows you how to make a change that needs no approval, and how to repeat it safely when an answer is lost. It works one tool through in full and then gives the other nine. [Safe writes and approvals](/mcp/writes.md) explains why these ten are separated from the nine that wait for you, and its result object section defines every field printed below.

Nine of the ten take an idempotency key and behave the same way. The tenth is `actions.cancel`, whose whole input is the identifier of the review request it withdraws. Sending it a key is refused before the tool runs.

## Send the write

`contacts.notes.update` replaces the private note on one of the student's own contacts. Send the new value and an idempotency key of 16 to 128 characters that you choose:

```json contacts.notes.update input theme={"system"}
{
  "contactId": "c0ffeec0ffeec0ffee000001",
  "idempotencyKey": "example-idempotency-key-0006",
  "notes": "Met at the spring fintech mixer. Follow up in April about the referral."
}
```

The answer says the change is done:

```json contacts.notes.update result theme={"system"}
{
  "tag": "completed",
  "scenario": "success",
  "operationId": "0123456789abcdef01234567",
  "operationStatus": "succeeded",
  "result": { "summary": "Contact notes updated", "itemCount": 1 }
}
```

The `result` object is a summary and a count. The written value is never sent back, so read the value with the matching read tool if you need to show the student what is stored. A write result is never larger than 4,096 bytes, which is why it carries a summary rather than the value.

## Poll a write that was accepted

A safe write can answer `accepted` rather than `completed`, which means the work is under way and nothing has been written yet. To poll it, call the same tool again with the same idempotency key and the same input. The reply is the current state of that change, and nothing is written twice.

Wait at least a second between calls, and stop when the result is `completed`, `failed`, `cancelled` or `uncertain`. Those four are the states nothing moves out of.

## Replay after a lost answer

Send the identical call again, the same tool, the same key and the same input, and the answer is the same completed result. Nothing is written a second time:

```json contacts.notes.update result theme={"system"}
{
  "tag": "completed",
  "scenario": "success",
  "operationId": "0123456789abcdef01234567",
  "operationStatus": "succeeded",
  "result": { "summary": "Contact notes updated", "itemCount": 1 }
}
```

This is the behaviour to rely on when an answer is lost in transit. Retry the same call rather than building a new one.

## Recognise a reused key

Send the same key with different input, here a different note on the same contact, and the call is refused. Nothing is written and the stored note is untouched:

```json contacts.notes.update result theme={"system"}
{
  "status": "failed",
  "scenario": "conflict",
  "operationId": "0123456789abcdef01234567",
  "error": {
    "code": "VERSION_CONFLICT",
    "retryable": false,
    "correlationId": "0123456789abcdef01234567"
  }
}
```

A key names one change. Give a genuinely new change a new key.

## Handle the retry hint

A safe write can be accepted and still ask you to come back. The answer is an accepted result with a hint beside it, and nothing has failed:

```json Accepted result with a retry hint theme={"system"}
{
  "structuredContent": {
    "tag": "accepted",
    "scenario": "loading",
    "operationId": "0123456789abcdef01234567",
    "operationStatus": "processing"
  },
  "_meta": {
    "coffeechats/retry": {
      "code": "PERMIT_BACKPRESSURE",
      "retryAfterMs": 1000,
      "retryable": true
    }
  }
}
```

Wait for `retryAfterMs` milliseconds and call the tool again with the same idempotency key.

## The other nine safe writes

Each of the first eight takes the same idempotency key and answers with the same shapes. The ninth, `actions.cancel`, takes no key at all. Every tool's own reference page carries its full input table, including the bounds the executor enforces on each field.

> **profile.update**
>
> Sets one profile preference per call: the email length, the headline or the about-me summary.
>
> ```json profile.update input theme={"system"}
> {
>   "headline": "Economics student at Dartmouth",
>   "idempotencyKey": "example-idempotency-key-0015"
> }
> ```
>
> The completed summary reads `Email length preference updated`, `Headline updated` or `Summary updated`.

> **profile.job\_search\_stage.update**
>
> Sets the job-search stage, the experience level and the recruiting timeline together.
>
> ```json profile.job_search_stage.update input theme={"system"}
> {
>   "experienceLevel": "college_junior",
>   "idempotencyKey": "example-idempotency-key-0004",
>   "recruitingTimeline": "this_term",
>   "seekingStage": "internship"
> }
> ```
>
> The completed summary reads `Job-search stage updated`.

> **contacts.tags.update**
>
> Replaces the whole tag list on one contact, at most 25 tags of 60 characters each. The list is replaced rather than merged, so send every tag you want to keep.
>
> ```json contacts.tags.update input theme={"system"}
> {
>   "contactId": "c0ffeec0ffeec0ffee000001",
>   "idempotencyKey": "example-idempotency-key-0007",
>   "tags": ["fintech", "warm"]
> }
> ```
>
> The completed summary reads `Contact tags updated`.

> **meetings.notes.update**
>
> Replaces the private preparation notes on one meeting, at most 10,000 characters. This is the meeting's own notes field and not the note the outcome form captures. A practice session is not writable.
>
> ```json meetings.notes.update input theme={"system"}
> {
>   "idempotencyKey": "example-idempotency-key-0008",
>   "meetingId": "c0ffeec0ffeec0ffee000002",
>   "notes": "Ask about the summer internship pipeline and how referrals are submitted."
> }
> ```
>
> The completed summary reads `Meeting notes updated`.

> **meetings.outcome.update**
>
> Records the star rating from 0.5 to 5 in half steps, the referral outcome, up to 10 key takeaways and the outcome notes, all together. It does not mark a thank-you as sent, it does not schedule a follow-up and it does not move the linked contact. The student does those in the application.
>
> ```json meetings.outcome.update input theme={"system"}
> {
>   "idempotencyKey": "example-idempotency-key-0009",
>   "keyTakeaways": ["They offered to refer me in April."],
>   "meetingId": "c0ffeec0ffeec0ffee000002",
>   "outcomeNotes": "A warm conversation that ended with a clear next step.",
>   "rating": 5,
>   "referralOutcome": "GOT_REFERRAL"
> }
> ```
>
> The completed summary reads `Meeting outcome updated`.

> **campaign\_pause\_sending**
>
> Stops a campaign from sending more mail. Read the bounds on the [campaign pause tool](/mcp/tools/campaign-pause-sending.md) page before you promise a student anything: it is a hold rather than a withdrawal, mail a send-now batch has already queued is still dispatched, and nothing already sent can be recalled. There is no tool that resumes sending, so the student does that in the application.
>
> ```json campaign_pause_sending input theme={"system"}
> {
>   "campaignId": "c0ffeec0ffeec0ffee000003",
>   "idempotencyKey": "example-idempotency-key-0010"
> }
> ```
>
> The completed summary reads `Campaign sending paused`.

> **campaigns.details.update**
>
> Rewrites one campaign's name, description and goal. All three are written together and all three are required, so read them with `campaigns.get` first and send back the values you are keeping. The name is at most 100 characters and may not be blank, the description and the goal at most 500 each. Nothing about who the campaign contacts, what it sends or when it sends changes.
>
> ```json campaigns.details.update input theme={"system"}
> {
>   "campaignId": "c0ffeec0ffeec0ffee000003",
>   "description": "Outreach to fintech alumni who graduated in the last five years.",
>   "goal": "Book five coffee chats before the end of the term.",
>   "idempotencyKey": "example-idempotency-key-0011",
>   "name": "Spring fintech outreach"
> }
> ```
>
> The completed summary reads `Campaign details updated`.

> **jobs.applications.stage.update**
>
> Moves one internship application to a stage and a priority. Both are written together and both are required, so read them with `jobs.applications.get` first. The [job tools](/mcp/tools/jobs.md) page lists the eight stages and the three priorities, and neither field can be cleared. Moving off the saved stage for the first time records the applied date, exactly as the application does.
>
> ```json jobs.applications.stage.update input theme={"system"}
> {
>   "applicationId": "c0ffeec0ffeec0ffee000004",
>   "idempotencyKey": "example-idempotency-key-0012",
>   "priority": "HIGH",
>   "stage": "APPLIED"
> }
> ```
>
> The completed summary reads `Application stage updated`.

> **actions.cancel**
>
> Withdraws a request the same connection proposed. It is the one write tool that takes no idempotency key, because the identifier it takes already names one request. [Withdraw a review request](/mcp/guides/withdraw-a-request.md) is the procedure.

## Next steps

> **[Propose a booking page change](/mcp/guides/propose-a-booking-page-change.md)**
>
> The five changes that need the student's approval.

> **[Settle an uncertain result](/mcp/guides/settle-an-uncertain-result.md)**
>
> What to do when Coffee Chats will not say whether a write landed.


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