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.
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 anidempotencyKey, 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.
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 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 understructuredContent 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:
Completed write result
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.
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
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. Therequires_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 lists the shapes that tool can return. Report anything else as an unrecognised answer instead of acting on it.
What each outcome means
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 onlyoperationId, 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 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.
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 withoperationId, 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.
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 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.
Next steps
Approvals
What the review page shows, and the nine states a review request can be in.
Update a preference safely
One safe write worked through, with its replay and its conflict.
Propose a booking page change
The agent’s side of one approval, end to end.
Error codes
Every code a write can return, its cause and the action to take.

