Calendar

The user's real schedule, with any changes the agent proposes shown in context before anything is written.

Pattern

โ‰ˆ 2,442 tokens per use (tool 1,344 ยท guidance 360 ยท typical payload 738)How token costs are estimated

Example: Calendar

What does my week look like?

Your week

Oct 12โ€“18, 2026 ยท Times in America/New York (GMT-4)

  • Work
  • Personal
  • Team
View

All day
Mon12
Tue13
Wed14, today
Thu15
Fri16
Sat17
Sun18

When to use

  • The user asks what their day, week or month looks like.
  • The user asks "What's on today?" and wants a glance, not a grid: use view: "today".
  • The agent wants to change their calendar (move, add or cancel events), and the user should see the changes in context before they're written.
  • A plan has times that interact with existing meetings: focus blocks, an offsite agenda, rescheduling.

When not to use

  • The user needs to pick one free time for a new booking. Use the Slot Picker.
  • The agent hasn't read the calendar. Fetch events first; never invent a schedule.
  • The question has a one-line answer, e.g. "When is my next meeting?". Say it in text.

How the agent should use it

  • Every event comes from the calendar connector, with its real id, times and calendar.
  • Set timezone to the user's zone and give times with offsets.
  • For view: "today", set range.start to that day and pass that day's events.
  • Pass the whole range you fetched, including free events, so overlaps and conflicts are accurate.
  • Use proposals for every change, each with a one-line reason. For move and cancel, set eventId to the existing event.
  • Without proposals the calendar is display-only and returns nothing.
  • Write only the accepted proposal IDs from an apply result. On revise or dismiss, write nothing.

Interaction

Views. Day and week show a time grid: hours from workingHours with one hour either side (grown to fit every event), an all-day row on top, overlapping events side by side, and a line at the current time with its clock time pinned in the hour column. The day headers are tiles (the weekday over the date in large figures, today filled), and the grid has only faint hour lines: no column lines, no out-of-hours shading. Events are borderless blocks on a soft neutral fill with 10px corners; a free event is hollow with a border and a proposal is dashed. Month has only row lines. The hours scroll inside the card (a time grid scrolls by nature, D22; 24rem, 20rem when proposals sit below it) with the day names and all-day row pinned; it opens on the earliest proposed change, else the current time, else the start of the working day. The agenda scrolls inside the card too: it can span up to 62 days, so it is scroll-by-nature under D22. Month shows each day's events; a day with more than five shows three and a +N more button that expands it in place. Agenda lists events grouped by day. Today (view: "today") is a compact card for one day: the weekday and a large day number on the left; on the right, all-day events as pills, then the day's events stacked in order as rounded blocks whose height follows the duration (a 1-hour block is taller than a 30-minute one; every block fits at least two lines; long days scroll inside the card). Each block shows the title and the time range ("10:00โ€“11:00 AM"); an event with no duration shows just its title. Events sit on a soft fill; the one happening now has a slightly stronger fill and Now, or if none is running the next one is labelled Next; events already over are dimmed. A dot and a hairline mark the current time between the events that are over (or running) and the ones still to come. Free events are hollow with a border and labelled Free; proposals are dashed and labelled Proposed. It has no view switch, and selecting a block opens the same details card. The view switch only offers views that make sense for the range, and on narrow screens the week view becomes the agenda.

Navigation. Previous, next and Today move between periods only when the provided events reach them. Otherwise the header just shows the given range. Navigation is local view state; it never fetches.

Overlapping events share a column side by side; a block narrower than a full column gives its title two lines and drops the time and the calendar marker (both stay in the details card and the accessible name). The calendar legend sits beside the time zone in the header.

Proposals. Proposed events are dashed outlines labelled Proposed. A move shows the original faded and struck through, with the new slot labelled Moved here (each block shows one note, the most important: a conflict first, then the state; blocks too short for a note show an arrow and a Proposed tag; the full text is in the list below, the details card and the block's accessible name); a cancel strikes the event through. A proposal that overlaps a busy event says Conflicts with Design review next to a warning icon. Below the calendar, each change is one row in a single column, written out ("Move 1:1 with Dana ยท Tue 3:00 PM to Thu 3:00 PM") on one line with its conflict, if any, under it, and Accept and Decline toggles at the right, all accepted by default. The proposal's reason is shown in the event's details card (and read to screen readers in the list), not under each row. Declining a change removes it from the calendar straight away, and conflicts are rechecked against the remaining changes. Apply N changes sends the decision back; the component never writes to the calendar itself. Request changes is the message icon next to it.

Calendars are told apart by colour and by a small shape in the legend and on each event, so colour is never the only cue. An event is a solid block in its calendar's colour with its own text colour: slate blue, teal, gold and green for the first four calendars (after Google Calendar's dark theme, with deeper fills in light mode), and grey for any later calendar and for events with no calendar. Free events are hollow with a border, proposals are dashed and a cancelled or moved original is faded, so those states keep their look.

Details. Clicking an event, or pressing Enter on it, opens a small card above it, centred on the event and kept inside the screen; it fades and scales in and out (instantly with reduced motion). It shows the time, calendar, location, attendee count, and an Open link when url is a safe http(s) link. Focus moves into the card; the close button, Esc, a click elsewhere or a second click on the event closes it and returns focus to the event. Only one card is open at a time, and it never opens on hover: clicking a different event while a card is open slides the card to that event (no slide with reduced motion). It sits 12px clear of the event. Existing events can't be edited here.

Keyboard. Grid cells aren't focus stops. Events are buttons in chronological order with full labels, e.g. "Design review, Tuesday 2:00 PM to 3:00 PM, Work calendar, proposed". In month view the arrow keys move between days (up and down move a week). Escape closes the details card. View and range changes are announced.

Time zones. Days are bucketed in timezone with Intl only, so a week that crosses a daylight-saving change keeps its days and wall-clock times, and the header shows the offset ("GMT-4", or both offsets across a change), with the full zone name as its tooltip and screen-reader text. All-day events use calendar dates with an exclusive end, and count as busy only when busy is true.

Installation

npx shadcn@latest add https://your-site.com/r/calendar.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { calendarTool } from "@/components/genui/calendar.schema"

const tools = {
  // With proposals, the call is completed by the user's decision in the UI.
  // Without proposals, give it an execute() that returns immediately.
  [calendarTool.name]: tool({
    description: calendarTool.description,
    inputSchema: calendarTool.inputSchema,
  }),
}
components/message.tsx
import { Calendar } from "@/components/genui/calendar"

function ToolPart({ part, addToolResult }) {
  if (part.type !== "tool-show_calendar") return null

  return (
    <Calendar
      {...part.input}
      status={part.state === "input-streaming" ? "streaming" : undefined}
      result={part.state === "output-available" ? part.output : undefined}
      onResult={(result) =>
        addToolResult({
          tool: "show_calendar",
          toolCallId: part.toolCallId,
          output: result,
        })
      }
    />
  )
}

When there are proposals, the agent receives one of:

{ action: "apply", accepted: ["move-dana", "focus-mon"], declined: ["focus-wed"] }
{ action: "revise", feedback: "Keep Wednesday mornings free" }
{ action: "dismiss" }

calendar-utils.ts exports the pure helpers, so the agent's backend can reuse them: applyProposals(events, proposals, accepted) returns the resulting event list, and findConflicts(events, proposals, timezone, accepted) returns the busy events each proposal overlaps.

With LangGraph

Push it from a graph node with ui.push({ name: "show_calendar", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

When a conversation is reloaded, pass the stored result back as result. After apply, accepted proposals are drawn as normal events labelled Added or Moved, cancelled and declined ones are gone, and each change is marked accepted or declined. After revise or dismiss, the proposals stay visible with the outcome in the footer. It never calls onResult again, so a decision can't be made twice.

<Calendar
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
titlestring?Defaults to the range label
timezonestringIANA zone, e.g. America/New_York
view"day" | "week" | "month" | "agenda" | "today"The first view. Default week. today is the compact one-day card
range{ start, days? }start is YYYY-MM-DD (for today, the day shown). days defaults to 1 (day, today), 7 or the rest of the month
eventsCalendarEvent[]Data-bound. Up to 300
calendars{ id, label }[]Data-bound. Legend order, up to 6
proposalsCalendarProposal[]Up to 20. Omit for a display-only calendar
workingHours{ start, end }HH:mm. Default 09:00โ€“18:00
nowstring?ISO time for the current-time line. Omit in real use

CalendarEvent (all data-bound)

FieldTypeNotes
idstringProposals refer to it
titlestring
start, endstringISO with offset; YYYY-MM-DD for all-day (end exclusive)
allDayboolean?
calendarstring?A calendar id or name
locationstring?
attendeesnumber?Count
busyboolean?Default true for timed events, false for all-day
urlstring?http(s) only; shown as Open

CalendarProposal

FieldTypeNotes
idstringReturned in the result
kind"create" | "move" | "cancel"
eventIdstring?Required for move and cancel
titlestring
start, endstringThe new time for move; the current time for cancel
reasonstring?One line. Shown in the details card

Host props: status (streaming, ready, submitting, applied, revised, dismissed), onResult, result and className.

Source

On this page