# Scheduling formats

Scheduling data reaches Connect as instants, not as calendar documents. Timestamps on the wire are RFC 3339 — the internet profile of ISO 8601 — in UTC. A follow-up carries a due instant and nothing else; business hours and timezone are applied when it is acted on. Connect emits no iCalendar.

- **Status:** Reference
- **Audience:** developer, both
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/protocols/ical-scheduling/

## Three formats people mean by 'a date'

**RFC 3339 instant** — `2026-09-10T09:15:00Z` — an unambiguous point in time, written as the internet profile of ISO 8601. This is what an API should carry.
**A local date and time** — `2026-09-10 09:15` in some place. Not a point in time until you know which place, and it moves when that place changes its offset.
**iCalendar (RFC 5545)** — A document format for calendar objects — events, alarms and recurrence rules — designed to be sent between calendar applications. A different kind of thing entirely from a timestamp.

Most scheduling bugs are a confusion between the first two. A time stored as an instant is stable; a time stored as local text is a guess about a rule that somebody else may change. The one legitimate use of the second form is a future appointment in a named place where the wall clock is what was agreed — and even then the timezone must travel with it.

## Does Connect use these formats?

RFC 3339, yes, everywhere on the wire. Event payloads carry instants in UTC with a trailing `Z` — a webhook's `occurredAt` and a follow-up's `dueAt` are both of that shape. Time in the public API follows the same rule and is documented at [Time in the API](/developers/time-and-timezones/).

iCalendar, no. Connect publishes no `.ics` file, no calendar feed and no subscription URL, and it parses no incoming calendar object. A `.ics` attached to an email is handled as a file like any other rather than interpreted as an event. Recurrence rules — the `RRULE` machinery that makes iCalendar hard — do not appear anywhere in the product.

What exists instead is the follow-up: a dated commitment on a channel, with a reason. Channels are `email`, `whatsapp`, `sms`, `phone`, `any` and `task`, the last meaning work for a person that no drain will ever send. See [Follow-ups in Connect](/docs/follow-ups/).

## Why a due time carries no business hours

`due_at` is an instant and only an instant. Quiet days, opening hours and the customer's timezone are the policy layer's business and are checked when the item is acted on, not baked into the date when it is created.

The reason is a failure that is obvious in hindsight. If a workspace's opening hours were folded into each due time at creation, changing those hours would leave every already-scheduled row pointing at the old ones — a hundred commitments quietly scheduled under a rule nobody uses any more, discoverable only when a customer is rung at the wrong hour.

The same separation keeps the follow-up queue from becoming a second authority. Whether Connect may act, on which channel and inside which hours, is autonomy's decision, asked afresh when the item comes due — not a property frozen into a timestamp days earlier.

| Question | Decided by | At what moment |
|---|---|---|
| When is this due? | The follow-up's `due_at` instant | When it is created |
| May Connect act at all? | Autonomy for that channel and scope | When it comes due |
| Is now an acceptable hour? | The policy layer | When it is acted on |
| Is it too late to bother? | The lateness ceiling | When it is drained |

## Lateness, and the commitment that is better dropped

A late call-back is not simply a call-back that happened later. Phone follow-ups drain a couple at a time, and a line that is not ready is retried in an hour; but a row more than 24 hours late is closed as missed and never rung. Ringing somebody a day and a half after promising to call reads worse than not ringing, and the queue is designed to admit that rather than clear itself at any cost.

- Store instants in UTC and convert only for display.
- Send an offset or `Z` on every timestamp. A bare local time in an API payload is a defect waiting for a daylight-saving change.
- Keep policy — hours, quiet days, allowances — out of the stored time, so changing policy changes behaviour rather than requiring a migration.
- Decide what 'too late' means and enforce it, rather than letting a backlog deliver yesterday's promises today.

## Questions

### Can I subscribe to Connect's follow-ups as a calendar feed?

No. There is no `.ics` output and no calendar subscription. Follow-ups are readable through the app and, for a machine caller, through the API and outbound webhooks — a `followup.due` event carries the identifier, channel, reason and due instant.

### Does Connect understand a calendar invitation sent by email?

Not as an event. An attached calendar file is treated as a file: it can be stored and read as text, but no appointment is created from it and no recurrence is expanded.

### What timezone are times in?

UTC on the wire, with a `Z`. What a person sees in the interface is rendered for them; what a program receives is an instant, which is the only form that cannot be misread.

## Related

- [Follow-ups in Connect](https://connectbyjbrh.com/docs/follow-ups/)
- [Time in the API](https://connectbyjbrh.com/developers/time-and-timezones/)
- [AsyncAPI](https://connectbyjbrh.com/docs/protocols/asyncapi/)
- [What Connect may do](https://connectbyjbrh.com/docs/autonomy/)
- [The public data model](https://connectbyjbrh.com/developers/data-model-public/)

## What this page is based on

- `backend/app/followups.py` — due_at, the channels and what it refuses to be
- `webapp/developers/asyncapi.yaml` — occurredAt and dueAt on the event envelope
- Source pack: Connect as a whole §7 — follow-ups
- `docs-source/facts.py` — MEASURED['phone_followup_max_late_hours']
