Calendar
The user's real schedule, with any changes the agent proposes shown in context before anything is written.
โ 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
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
timezoneto the user's zone and give times with offsets. - For
view: "today", setrange.startto that day and pass that day's events. - Pass the whole range you fetched, including free events, so overlaps and conflicts are accurate.
- Use
proposalsfor every change, each with a one-linereason. Formoveandcancel, seteventIdto the existing event. - Without proposals the calendar is display-only and returns nothing.
- Write only the accepted proposal IDs from an
applyresult. Onreviseordismiss, 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
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,
}),
}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
| Prop | Type | Notes |
|---|---|---|
title | string? | Defaults to the range label |
timezone | string | IANA 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 |
events | CalendarEvent[] | Data-bound. Up to 300 |
calendars | { id, label }[] | Data-bound. Legend order, up to 6 |
proposals | CalendarProposal[] | Up to 20. Omit for a display-only calendar |
workingHours | { start, end } | HH:mm. Default 09:00โ18:00 |
now | string? | ISO time for the current-time line. Omit in real use |
CalendarEvent (all data-bound)
| Field | Type | Notes |
|---|---|---|
id | string | Proposals refer to it |
title | string | |
start, end | string | ISO with offset; YYYY-MM-DD for all-day (end exclusive) |
allDay | boolean? | |
calendar | string? | A calendar id or name |
location | string? | |
attendees | number? | Count |
busy | boolean? | Default true for timed events, false for all-day |
url | string? | http(s) only; shown as Open |
CalendarProposal
| Field | Type | Notes |
|---|---|---|
id | string | Returned in the result |
kind | "create" | "move" | "cancel" | |
eventId | string? | Required for move and cancel |
title | string | |
start, end | string | The new time for move; the current time for cancel |
reason | string? | One line. Shown in the details card |
Host props: status (streaming, ready, submitting, applied, revised, dismissed), onResult, result and className.