GroupEventIntent v0.1
A GroupEventIntent is a coordination request across N principals. The protocol
defines five primitives. Each can be exchanged across A2A, embedded in tool-call payloads
for MCP, or transported however the calling agent prefers — the schema is the contract,
not the wire.
This page lists the five primitives in the order they're produced during the coordination lifecycle. Each carries a TypeScript type signature, a JSON shape, and a one-paragraph note on its purpose.
1. GroupEventIntent
Opens the coordination. One per planning attempt. Carries the request the organizer is making — group size, when, where (location plus a search radius), and a free-form message — plus an expiry so abandoned intents don't accumulate.
interface GroupEventIntent {
id: string; // UUID assigned by the resolver
creatorId: string | null; // Authenticated organizer, if any
creatorName: string; // Display name for the organizer
groupSize: number; // 1–N (golf cap = 4)
status: IntentStatus; // collecting | resolving | voting | winner | expired | cancelled
message: string | null; // Optional free-form context
lat: number; // Search anchor — lat
lng: number; // Search anchor — lng
radiusKm: number; // Search radius (km)
expiresAt: string; // ISO 8601 timestamp
createdAt: string;
updatedAt: string;
} The status field is the only mutable column on a live intent — every other
field is immutable post-creation. The state machine is strictly forward; the resolver
rejects re-resolution from terminal states (winner, expired, cancelled).
2. Constraint
One per principal in the group. Each peer agent submits exactly one constraint set on
behalf of its human, communicating that person's per-date availability windows and
preferences. The intent advances to resolving automatically once constraints.length === groupSize.
interface Constraint {
id: string;
intentId: string;
playerName: string;
dates: Array<{
date: string; // YYYY-MM-DD
earliestTime: string; // HH:MM
latestTime: string; // HH:MM
}>;
// Optional preferences — all may be null/undefined
maxPriceCents: number | null;
cartRequired: boolean;
// Vertical extensions live here; see GolfIntent
} The dates array is the authoritative source of availability. The same time
window can apply to every date ("default across the week") or vary per-day ("free all
morning Tue, only 4–6pm Wed"). A submission is the principal's complete schedule —
downstream resolvers should treat re-submissions as full replacements.
3. Option
A resolved match against the constraint set. The resolver intersects every constraint and ranks the surviving candidates by group-fit (date overlap, time overlap, price ceiling, vertical-specific preferences, distance, current weather). 3–5 options is typical; the array can be empty when no candidate satisfies every constraint.
interface Option {
id: string;
intentId: string;
rank: number; // 1 = best fit
score: number; // 0..1, monotonic with rank
date: string; // YYYY-MM-DD
time: string; // HH:MM
priceCents: number;
bookingUrl: string; // Deep-link into the venue's booking surface
voteCount: number; // Updated as votes land
isWinner: boolean; // Set when group is fully voted; tied options carry true
// Vertical-specific fields live here (course, holes, cart, weather, etc.)
} Options are produced once per resolve and frozen — re-resolution replaces the entire
set atomically. Vote counts are mutable until the intent transitions to winner; once there, they're frozen alongside the option set.
4. Vote
A principal's preference over the resolved options. Votes are immutable once the intent
reaches winner state, but mutable while it's still voting: a
player can change their mind and re-submit until everyone has voted. Each principal
gets exactly one vote regardless of how many times they re-submit.
interface Vote {
intentId: string;
playerName: string;
optionId: string;
votedAt: string; // ISO 8601
} Ties are not broken automatically. A 1-1 split or 2-2 split leaves multiple
options with isWinner: true — the calling agent or UI should surface "no
clear winner, talk with your group" rather than picking arbitrarily.
5. BookingProposal
The terminal artifact. Once the group has converged on a winner, the BookingProposal is the handoff to the actual booking surface. Limlock does
not complete the booking itself; it produces the proposal, the calling agent or human
follows the deep-link.
interface BookingProposal {
intentId: string;
optionId: string;
bookingUrl: string; // Most-specific deep-link the venue platform supports
priceCents: number;
date: string;
time: string;
// Receipt of the handoff lives in the coordination receipt log
// — see /spec/receipts
} Downstream payment protocols (e.g. AP2) close the loop from this point on. Limlock composes with payment protocols rather than replacing them — see /spec/payments.