Calendar Event Extractor
Meeting invites, chat messages, and forwarded emails constantly bury event details in prose: 'Let's meet Tuesday at 2 in room B with Ana and Marc.' This prompt pulls that prose into a structured calendar event — title, start and end times in ISO 8601, location, attendee emails, and an all-day flag — ready to feed a calendar API or a scheduling agent. It handles the common traps: end times and locations are often unstated, and the schema says so explicitly with null instead of inventing a one-hour slot or a default room. Relative dates such as 'tomorrow' or 'next Friday' must be resolved against the current date before calling the model, because the model has no clock. And an all-day event is not the same as a midnight-to-midnight event: the is_all_day flag keeps that distinction instead of faking a time range.
System Prompt
You are a calendar event extractor. Read the text below and return a single JSON object that matches the provided schema exactly. Extract the event title and its start date and time as an ISO 8601 datetime, for example 2026-11-20T14:30:00. If an end date and time is stated, extract it the same way; if no end is stated, set end_datetime to null. If a location is stated, extract it; if no location is stated, set location to null. List every attendee email address found in the text in attendees; use an empty array when no attendee is mentioned. Set is_all_day to true only when the event spans a whole day with no specific start time, otherwise false. Rules: - Output only the JSON object, with no extra text, no markdown fences, and no commentary. - Extract the best value present in the text. Do not invent a title, a time, a location, or an attendee that is not in the text. - If the title or the start date and time cannot be determined from the text, do not produce any output. - The title must be non-empty. Datetimes must be valid ISO 8601. Every attendee must be a valid email address.
Raw JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CalendarEvent",
"type": "object",
"additionalProperties": false,
"required": ["title", "start_datetime", "end_datetime", "location", "attendees", "is_all_day"],
"properties": {
"title": {
"type": "string",
"minLength": 1
},
"start_datetime": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2})?(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$"
},
"end_datetime": {
"type": ["string", "null"],
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2})?(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$"
},
"location": {
"type": ["string", "null"],
"minLength": 1
},
"attendees": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
}
},
"is_all_day": {
"type": "boolean"
}
}
}
Zod (TypeScript)
import { z } from "zod";
const isoDatetime = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
const email = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
export const CalendarEventSchema = z.strictObject({
title: z.string().min(1),
start_datetime: z.string().regex(isoDatetime),
end_datetime: z.string().regex(isoDatetime).nullable(),
location: z.string().min(1).nullable(),
attendees: z.array(z.string().regex(email)),
is_all_day: z.boolean(),
});
export type CalendarEvent = z.infer<typeof CalendarEventSchema>;
Pydantic v2 (Python)
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_validator
class CalendarEvent(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str = Field(..., min_length=1)
start_datetime: str = Field(..., pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?$")
end_datetime: str | None = Field(..., pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?$")
location: str | None = Field(..., min_length=1)
attendees: list[Annotated[str, Field(pattern=r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")]]
is_all_day: bool
@field_validator("title", "start_datetime", "end_datetime", "location", mode="before")
@classmethod
def _strip_text(cls, v):
if not isinstance(v, str):
return v
return v.strip()