# Business Hours & Timezones — stop the agent from doing clock math

> The agent has to know what time it is: human hand-off windows ("atendemos Lun–Vie 9–15"),
> after-hours disclaimers, "no ofrezcas Calendly el finde". The failure mode is always the same —
> the **prompt** is made to do timezone arithmetic, the model narrates or botches it, and the lead
> sees the machinery (or gets offered a slot nobody will honor). It doesn't have to: v5 agents ship
> a native `get_current_time` tool, and there is a real-availability path via Google Calendar.
> Pairs with `knowledge/reasoning-leak.md` (a time-window branch is a leak-prone branch),
> `knowledge/workflows.md` (workflow windows are a *different*, UTC-based knob), and
> `knowledge/platform-features.md` §8.

## The rule

**Never express a business-hours window in UTC in the prompt, and never ask the model to convert.**
Timezone arithmetic in an LLM is a reliable source of errors — worst of all across day boundaries
(a lead writing Friday 23:00 ARG is Saturday 02:00 UTC; the agent decides it's the weekend and
kills the offer). Write the window in the **business's local time** and let the runtime hand you
local fields to compare against.

Two levers, pick by what the window actually is:

| The window is… | Use | Why |
|---|---|---|
| A fixed schedule the operator knows ("Lun–Vie 9–15") | **`get_current_time`** + `v5Config.timezone` | Zero integration; the agent just reads the clock in the right zone. |
| Whoever-is-free, from a real calendar | **Google Calendar tool** (`listFreeCalendarSlots`) | Returns actual free slots — respects meetings already booked, not just office hours. |

## 1 — `get_current_time` (the clock)

A **native v5 tool**, in the tool array of *every* v5 agent — nothing to install. Called with no
arguments; returns a JSON string:

```json
{
  "utc_iso": "2026-05-20T02:00:00.000Z",
  "utc_date": "2026-05-20", "utc_time": "02:00",
  "utc_weekday": "wednesday", "utc_weekday_num": 3,
  "timezone": "America/Argentina/Buenos_Aires",
  "local_date": "2026-05-19", "local_time": "23:00",
  "local_weekday": "tuesday", "local_weekday_num": 2
}
```

- `*_weekday_num` is **ISO**: Mon=1 … Sun=7.
- **Always branch on the `local_*` fields.** The `utc_*` fields exist for logging/debugging.
- **Unset timezone → everything is UTC** and `local_* == utc_*`. That is the silent default, and it
  is exactly the state an agent is in until someone sets the zone.
- **Invalid IANA string → silent fallback to UTC** (logged to Sentry, never surfaced to the agent).
  A typo doesn't error — it just quietly makes the agent wrong. Verify the value, don't assume.
- The tool being *available* doesn't mean the agent *uses* it. The system prompt's tools section is
  the de-facto allowlist: **if you don't list `get_current_time` there and tell it when to call it,
  it won't.**

### Setting the zone — `v5Config.timezone`

One IANA string on the agent's v5Config, e.g. `"America/Argentina/Buenos_Aires"`.

> ⚠️ **Not exposed by MCP today — an external prerequisite, not the workspace path.**
> `deploy_agent_sdk` and `update_v5_config` don't accept a `timezone` field, and unknown keys are
> stripped on the way in, so passing it does nothing, silently. Until the gateway exposes it, the
> zone has to be set in the **agent's config JSON in Studio**.
>
> This is a genuinely missing capability, so **name it and file it** — `report_feedback` with
> `type: FEATURE_REQUEST` and a `reason` that says what you were doing ("necesitaba setear la
> timezone del agente X para gatear la ventana de atención y `deploy_agent_sdk` no expone el
> campo"), not a generic "falta timezone". Reporting it doesn't replace giving the operator the
> working result this turn: point them at the exact Studio field, then keep going. See
> `knowledge/feedback-reporting.md` and `knowledge/self-sufficiency.md`.

Whatever sets it, **verify with `get_agent_config`**: `config.v5Config.timezone` must be the exact
IANA name. Keep the local working copy honest by recording it in `agents/<name>/sdk/runtime.json`
(`{ "timezone": "America/Argentina/Buenos_Aires" }`) — the SDK-side home for runtime knobs the tools
read, distinct from the 8 content sections.

### The prompt pattern

Put the gate where the decision happens (the close / the hand-off), phrased as a lookup, not as math:

```text
ANTES DE OFRECER LA LLAMADA (obligatorio):
1. Llamá `get_current_time`.
2. Si `local_weekday_num` va de 1 (lunes) a 5 (viernes) inclusive, Y `local_time` >= "09:00"
   Y `local_time` < "14:00" → ofrecé el link.
3. Si no → no ofrezcas el link. Decile que retomamos agendas el lunes desde las 9hs y dejá la
   conversación abierta para el follow-up.
```

**State both endpoints explicitly.** "Entre 09:00 y 14:00" leaves the model to guess whether 14:00
is in or out, and it will guess differently across conversations — right at the cutoff, which is the
one moment the gate exists for. `local_time` is a zero-padded 24h `"HH:mm"` string, so plain
comparison sorts correctly; pick inclusive-start / exclusive-end and keep it consistent.

Two things that came out of the first production use of this — an agent handing out a 24/7 Calendly
for a human available Lun–Vie 9–15, which produced a **75% no-show rate**:

- **Close the window before the human's real cutoff.** They stop at 15:00; the gate stays open only
  until 14:00. A lead who lands at 14:55 books a slot nobody takes same-day and no-shows. The
  buffer is a judgment call — **write the reason in `changelog.md`**, or the next operator "fixes"
  it back.
- This replaces the manual prompt edit every Friday. If you find yourself editing a prompt on a
  schedule, that's this tool's job.

### It overlaps with reasoning leak

A time window is a **conditional rule with a literal threshold evaluated at a branch point** — the
exact shape that makes the model narrate its own logic ("son las 23:04 en Argentina, fuera de la
ventana de atención, aplicar guard"). See `knowledge/reasoning-leak.md`. So:

- **Demonstrate the out-of-hours reply in `examples.md`** — a real closed-window exchange, in the
  agent's voice. That, not the rule text, is what the model copies.
- Avoid the leak vocabulary in the rule: no "verificar", no "→ guard", no restating the threshold
  back at the lead.
- Keep the CoT `response_filter` on (`update_agent_config`) as defense in depth.

## 2 — Google Calendar (real availability)

When the ask is "only offer times the human can actually take", office hours aren't enough — you
need the calendar. The `listFreeCalendarSlots` agent tool queries Google free/busy and returns
genuinely open slots.

**External prerequisite:** the Google OAuth connection is done once per agent in Studio.
`update_tool` explicitly refuses to write `credentials`, so this step is not MCP-drivable and no
amount of retrying will make it so — say that plainly and point at Studio rather than looping. Once
the account is connected, **the rest is MCP**: `enabled` and the whole `settings` object are
writable.

```text
list_agent_tools(agent_id)                 → tool_id + current settings
update_tool(tool_id, settings: {...})      → settings is a free-form object (credentials are not writable)
```

Settings that matter:

| Key | Meaning |
|---|---|
| `timezone` | IANA zone the working hours are interpreted in. **Separate from `v5Config.timezone`** — set both, to the same value. |
| `workingHours` | `{ "1": {start:"09:00", end:"15:00"}, ... }` — keys are ISO weekdays (Mon=1…Sun=7). **A day with no key is closed**, which is how you express Lun–Vie. |
| `eventDurationMinutes` | Slot length (default 30). |
| `daysAheadLimit` | How far ahead to look (default 7). |
| `onlyFromNextDay` | Drop same-day slots — the calendar equivalent of the end-of-day buffer above. |
| `eventSummary` / `eventDescription` / `eventLocation` | What gets written on the booked event. |

Slot generation starts at the **next full hour**, so "right now" is never offered.

## 3 — Other clocks on the platform (don't mix them up)

Three surfaces say "time" and none of them read `v5Config.timezone`:

- **Workflow execution windows** (`executionWindowStart/End` on a FOLLOW_UP) — the gateway's
  `upsert_workflow` schema documents these as **UTC**, not the lead's or the operator's local time.
  A 08:00–22:00 Chile (UTC-4) window is `"12:00"`–`"02:00"`, and yes it wraps past midnight (that's
  supported). See `knowledge/workflows.md` §Execution Rules.
- **Trigger negative-config `user_timezone`** — an authoring/display convenience. The expiry is an
  absolute instant; picking a zone changes how it's shown, not when the block lifts.
- **The scheduling agent** asks the *lead* where they are, because it books in the lead's zone.
  That's per-conversation and unrelated to the business's zone.

## Checklist

- [ ] `get_agent_config` → `config.v5Config.timezone` is the correct IANA name (not absent, not a typo).
- [ ] The system prompt lists `get_current_time` and says **when** to call it.
- [ ] Windows are written in **local** time and compared against `local_weekday_num` / `local_time`.
- [ ] The out-of-hours reply exists in `examples.md`, not only as a rule.
- [ ] Buffer before the human's real cutoff, with the reason in `changelog.md`.
- [ ] If Google Calendar is on: `settings.timezone` matches `v5Config.timezone`, and `workingHours`
      omits closed days.
- [ ] Any follow-up window on the same schedule is expressed in **UTC** (workflows), not local.
