The Tool Returned 200 and Nothing Happened

A calendar agent reported success on a meeting that landed at 3am, on the wrong calendar, with no invitation sent. The API did exactly what it was asked — and the return value gave the agent no way to know.

"Move the 3pm to Thursday and invite Priya." The assistant replies: done, moved to Thursday at 3pm, Priya invited.

Thursday arrives. Priya has never heard of the meeting, and the event is sitting on the calendar at 3:00 in the morning.

Every API call in that run returned 200. Nothing retried, nothing errored, nothing appeared in the logs worth looking at.

Why the agent was confident

The agent saw this:

{"status": "ok", "id": "evt_8823"}

That is the complete basis for "done, moved to Thursday at 3pm, Priya invited." The rest was inferred from what it had asked for.

An agent never touches the world. It sends arguments to a function and reads what comes back, and that return value is the world as far as the model is concerned. Anything the tool doesn't mention is not something the agent overlooked — it is something the agent had no access to.

⚠️ Every field you leave out of a tool's response is a fact the agent will substitute with its own assumption, and its assumption is always that the request succeeded exactly as intended.

Four ways a calendar write succeeds and fails

The timezone. The tool passed "2026-08-27T15:00:00" with no offset. The API accepted it and interpreted it in its own default zone. The event is real, on the right day, at 3am local. The response said ok because from the API's side, it was.

The wrong container. The integration authenticates as a service account with write access to several calendars. Without an explicit calendar id, the event was created on the service account's own calendar. The user's calendar is unchanged and the API is satisfied.

The invitation that wasn't sent. Attendees were attached to the event, but the notification parameter defaulted to sending nothing. The event lists Priya. Priya's inbox does not. Two different things, one of which the response reported.

The duplicate. The first call timed out on the client after the server had already committed the write. The framework retried, the second call succeeded, and there are now two Thursday meetings. The agent saw one success and reported one meeting.

In all four, the API did what it was asked. The gap was between the request and the intent, and the response had no room to express it.

The fix is in the return value, not the prompt

Adding "make sure to check the timezone" to the system prompt does nothing, because the agent has nothing to check against. There is no observation to compare the instruction to.

❌ What most write tools return:

{"status": "ok", "id": "evt_8823"}

✅ What makes the failure visible:

{
  "event_id": "evt_8823",
  "calendar": "user primary (asha@example.com)",
  "start": "2026-08-27T15:00:00+05:30",
  "start_human": "Thursday 27 Aug, 3:00 PM IST",
  "attendees": [
    {"email": "priya@example.com", "invite_sent": false,
     "note": "notifications disabled for this request"}
  ],
  "outcome": "created"
}

Nothing about the model changed. But now a mismatch between what was intended and what exists is sitting in the context as plain text, and the agent will say "created for Thursday 3 PM, but the invitation wasn't sent" without any instruction telling it to look.

💡 The read-back principle

After a write, return what a subsequent read would return, not what the write's status code was.

The write call reports transmission. A read reports state. Those coincide often enough that skipping the read feels safe, and they diverge in exactly the cases that matter: defaults applied, coercions performed, side effects suppressed.

The cost is one extra round trip, sometimes zero when the API already returns the created object. What it buys is that misdirected writes stop being silent.

Idempotency belongs to the tool

The duplicate case deserves its own treatment, because retries are increasingly not the agent's decision. Frameworks retry, queues retry, workflow engines retry. Any of them can fire after a write that already landed.

The instinct is to instruct the agent to check whether the event already exists before creating it. That fails for the same reason as the timezone instruction: it depends on the model remembering, every time, under every phrasing.

Put the key in the tool signature instead:

create_event(..., idempotency_key = hash(user_id, title, start, attendees))

The second call returns the same event with "outcome": "already_existed". Retries become free, and the agent gets told what actually happened rather than being asked to prevent it.

🔍 Auditing a write tool

Five questions per tool, and any "no" is a silent failure waiting:

  • Does the response name the container it wrote to — which calendar, which project, which folder?
  • Are timestamps returned with an explicit offset, plus a human-readable rendering the model can compare against what the user said?
  • Does it report side effects the caller cares about — notifications sent, webhooks fired, downstream records touched?
  • Is there a client-supplied idempotency key, owned by the tool rather than by the prompt?
  • On partial success, does it return partial, or does it return ok?

The hard one: partial success

Event created, invitation failed. Returning ok throws away the second half. Returning an error implies the first half didn't happen either, and an agent reading an error will often try again — creating a second event.

The only honest shape reports both:

{"outcome": "created",
 "warnings": [{"effect": "invitation", "status": "failed",
               "detail": "attendee address rejected"}]}

Ugly, and correct. The agent can tell the user what exists and what still needs doing, which is the whole job.

The model's picture of the world is exactly the union of what your tools have said to it. Write return values as reports of state, not receipts of transmission.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.