Anvil
Meeting times across all your calendars
Meeting times, hammered into shape (module goforge.dev/anvil). Anvil finds
slots that work across every calendar each person lives by — personal,
work, startup, volunteer — for any mix of required and optional attendees,
with drive-time padding for in-person meetings.
v0.4.3 — Free scheduling and typed calendar transport
Anvil is fully free and MIT-licensed with unlimited scheduling links. The current release uses the replayability-safe typed Resty rewrite for calendar feeds and explicit response-body ownership, while retaining demo mode, the agenda PWA, CalDAV and Google Calendar integration, Meet links, and deployable self-hosting. The v0.4.3 patch consumes the released Go+ stdlib v0.207.0.
Existing schedulers assume one person ≈ one calendar, maybe two. The real shape of the problem is people × calendars: you have four calendars, your co-founder has three, and “find a time for Mike and Melissa and Stan” means none of the ten may conflict. Anvil’s model is exactly that — a person is a name plus the union of all their busy time, and the finder never needs to know how many calendars fed it.
Install
go install goforge.dev/anvil/cmd/anvil@latest
Use it
anvil find -d 45m -from 2026-06-15 -to 2026-06-19 -tz America/New_York \
-who mike=work.ics,personal.ics \
-who melissa=https://calendar.google.com/calendar/ical/…/basic.ics \
-opt stan=stan.ics -opt david=david.ics \
-travel 30m
Wed Jun 17 13:30–14:15 EDT 4/4
Thu Jun 18 10:30–11:15 EDT 4/4
Thu Jun 18 11:00–11:45 EDT 3/4 missing: stan
Every -who is required; every -opt is scored — slots with more optional
attendees rank first, earliest wins ties. -travel 30m demands a clear half
hour on both sides of the meeting (drive time) without widening the meeting
itself. Calendars are iCal files or URLs; every major provider (Google,
Fastmail, iCloud, Proton) exports a private iCal address — no OAuth needed.
Flags: -d duration, -from/-to date window, -hours 09:00-17:00,
-days mon,tue,… (default weekdays), -step start granularity, -n max
results.
Scheduling links: anvil serve
Self-hostable Calendly-for-many-people. One JSON config wires calendars
(iCal URLs, CalDAV, Google) to people, people to links, and each link to
the calendar the booked invite lands in. Hand out https://your.host/l/intro
and J-random person picks from times that clear every calendar of every
required attendee:
"links": [
{"slug": "intro", "title": "Intro with Mike & Melissa", "duration_m": 45,
"required": ["mike", "melissa"], "optional": ["stan"],
"google_meet": true, "book_into": "melissa"},
{"slug": "onsite", "title": "Coffee with Mike", "duration_m": 60,
"required": ["mike"], "in_person": true, "travel_m": 30,
"address": "300 Webster St, Oakland", "book_into": "mike-personal"}
]
Booking re-verifies against live calendars (409 if the slot just vanished),
then creates the invitation with the right conferencing on it: a Google Meet
link minted via the API, a static room (video_url), or an in-person
address with drive-time padding on both sides. Invitations reach attendees
through the target calendar (Google sendUpdates, CalDAV server
scheduling).
Agenda: desktop, mobile, terminal
anvil serve also hosts an installable agenda app (PWA — add to a phone
home screen or install from a desktop browser): everything coming across
all configured calendars, each entry with its Join link (Meet, Zoom,
Teams, Webex, Jitsi, Whereby auto-detected) or Directions button.
Optional basic auth keeps the agenda private while scheduling links stay
public. The same view lives in the terminal:
anvil agenda -cal mike=work.ics -cal mike=personal.ics -days 3
Setup helpers: anvil gcal-login (one-time OAuth, prints the refresh
token), anvil caldav-calendars (lists collection URLs for the config).
Library
Six packages, nothing outside the Go standard library:
interval— half-open span algebra:Normalize,Union,Intersect,Subtract,Complement,Shrink.schedule— the kernel.Find(Request)intersects required attendees’ free time with a working-hours mask, applies travel padding by shrinking free intervals (so a padded meeting fits iff the slot survives), and ranks candidates by optional attendance. Pure and deterministic: busy sets in, slots out.ics— a small, honest iCalendar reader and writer: VEVENT, IANATZID, all-day events,DURATION,EXDATE, attendees/organizer, and DAILY/WEEKLY recurrence;Encodeemits RFC 5545 for invites and CalDAV PUTs.caldav— minimal CalDAV client: discovery, calendar-query REPORT for busy time, and create-only event PUT (If-None-Match: *). Fastmail, iCloud, Nextcloud, Radicale.gcal— minimal Google Calendar client: refresh-token OAuth, free/busy, event listing, insertion with Meet conference creation, and the loopback login flow.agenda— merges occurrences across calendars and attaches the join URL or a directions link per item.serve— the scheduling-link server and agenda app, built on all of the above.
cal, _ := ics.ParseIn(feed, tz)
mike := schedule.Merge("mike", cal.Busy(window), otherBusy)
slots, _ := schedule.Find(schedule.Request{
Duration: 45 * time.Minute,
Window: window,
Travel: 30 * time.Minute,
Required: []schedule.Person{mike, melissa},
Optional: []schedule.Person{stan, david},
})
Free and MIT-licensed
Every command, an unlimited number of scheduling links, all calendar sources, and the agenda PWA — free, with no tiers and nothing to activate. The source is MIT-licensed; self-host it on a $5 VPS and run as many links as you like.
Roadmap
Library: interval algebra, scheduling kernel, iCal ingestion— v0.1.0CLI:— v0.1.0anvil findover iCal files/URLsCalDAV and Google Calendar API adapters (read and write — create the invite, not just find the slot)— v0.2.0— v0.2.0anvil serve— self-hostable scheduling links with conferencing links on the invite and travel-time-aware in-person optionsUI: at-a-glance agenda across all calendars, join links and directions one tap away (installable PWA + terminal)— v0.2.0
Next up: ATTENDEE free/busy via iTIP, recurring link round-robin, and native wrappers if the PWA ever feels insufficient.
Non-goals: Exchange, Outlook, being a calendar.