Loading...
Loading...
MANDATORY recipe for every Caffeine build that lists upcoming events or creates events on the user's own Google Calendar. The ONLY supported path is the `googlecalendar-client` mops package (Calendar REST API v3) combined with the `google-oauth` mops package (token exchange + refresh + PKCE). Hand-rolling `ic.http_request` calls to `oauth2.googleapis.com` or `www.googleapis.com/calendar/v3` is a FORBIDDEN anti-pattern — it bypasses bearer auth, replication-cost safeguards, and the `google-oauth` library's percent-encoding and JSON parsing. Load this skill whenever the user, spec, or any prior task mentions scheduling, calendar events, appointments, meetings, "add to calendar", or any equivalent phrasing — and BEFORE writing any code that touches a Google endpoint.
npx skill4agent add caffeinelabs/skills connector-googlecalendargooglecalendar-clientgoogle-oauthic.http_requestoauth2.googleapis.comwww.googleapis.com/calendar/v3googlecalendar-clientgoogle-oauthemail-calendar-events| User intent | Platform capability |
|---|---|
| Connect and list upcoming events | |
| Create calendar events | |
access_tokencaller : Principal#admingooglecalendar-clientgoogle-oauthhttp_requestoauth2.googleapis.comaccess_tokenrefresh_tokencaller : Principalmops add googlecalendar-client@0.1.3
mops add google-oauth@0.1.4
mops add caffeineai-authorization@1.0.0code_verifiercode_challengegoogle-oauthgoogle-oauth.buildAuthorizeUrlcodegoogle-oauth.exchangeAuthorizationCodeaccess_tokenrefresh_tokencallergoogle-oauth.refreshAccessTokenhttps://<app-domain>/connect/calendar| Scope | Purpose |
|---|---|
| Full read/write access to calendars |
| Read/write access to events only |
| Read-only access to calendars |
| Read-only access to events |
calendar.readonlyBoolMap<Principal, CalendarConnection>isMyCalendarConnectedstartCalendarOAuthcompleteCalendarOAuthlistUpcomingEventscreateEventdisconnectMyCalendarnot caller.isAnonymous()access_tokenrefresh_tokenCalendarConnectioncode_verifierredirectUristaterefresh_tokenrefresh_tokenaccess_tokenrefresh_tokenis_replicated = ?falseAuthorization: Bearer <token>idetagis_replicated = ?falseConfigaccess_tokenrefresh_tokensrc/backend/main.moincludesrc/backend/mixins/calendar-config.mosrc/backend/mixins/calendar-messaging.mosrc/backend/lib/calendar.mogooglecalendar-clientgoogle-oauthimport Map "mo:core/Map";
import Principal "mo:core/Principal";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import MixinCalendarConfig "mixins/calendar-config";
import MixinCalendarMessaging "mixins/calendar-messaging";
import LibCalendar "lib/calendar";
actor {
let accessControlState = AccessControl.initState();
include MixinAuthorization(accessControlState, null);
let calendarConfig = {
var clientId : Text = "";
var clientSecret : Text = "";
};
include MixinCalendarConfig(accessControlState, calendarConfig);
let calendarConnections : Map.Map<Principal, LibCalendar.CalendarConnection> = Map.empty();
let pendingCalendarFlows : Map.Map<Principal, LibCalendar.PendingOAuth> = Map.empty();
include MixinCalendarMessaging(calendarConfig, calendarConnections, pendingCalendarFlows);
};import AccessControl "mo:caffeineai-authorization/access-control";
import Runtime "mo:core/Runtime";
mixin (
accessControlState : AccessControl.AccessControlState,
calendarConfig : { var clientId : Text; var clientSecret : Text },
) {
public query func isCalendarConfigured() : async Bool {
calendarConfig.clientId.size() > 0;
};
public shared ({ caller }) func setCalendarCredentials(clientId : Text, clientSecret : Text) : async () {
if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
Runtime.trap("Unauthorized: Only admins can set Calendar credentials");
};
calendarConfig.clientId := clientId;
calendarConfig.clientSecret := clientSecret;
};
};import Map "mo:core/Map";
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import LibCalendar "../lib/calendar";
mixin (
calendarConfig : { var clientId : Text; var clientSecret : Text },
calendarConnections : Map.Map<Principal, LibCalendar.CalendarConnection>,
pendingCalendarFlows : Map.Map<Principal, LibCalendar.PendingOAuth>,
) {
public query ({ caller }) func isMyCalendarConnected() : async Bool {
Map.containsKey(calendarConnections, Principal.compare, caller);
};
public shared ({ caller }) func startCalendarOAuth(redirectUri : Text) : async Text {
if (caller.isAnonymous()) {
Runtime.trap("Sign in to connect Google Calendar");
};
if (calendarConfig.clientId.size() == 0) {
Runtime.trap("Calendar is not configured (admin must set credentials)");
};
await* LibCalendar.startAuthorize(
calendarConfig.clientId, redirectUri, caller, pendingCalendarFlows,
);
};
public shared ({ caller }) func completeCalendarOAuth(code : Text, state : Text) : async () {
if (caller.isAnonymous()) {
Runtime.trap("Sign in to connect Google Calendar");
};
if (calendarConfig.clientId.size() == 0) {
Runtime.trap("Calendar is not configured");
};
let ?pending = Map.get(pendingCalendarFlows, Principal.compare, caller) else {
Runtime.trap("No pending OAuth flow — call startCalendarOAuth first");
};
if (state != pending.state) {
Runtime.trap("OAuth state did not match the pending Calendar flow");
};
Map.remove(pendingCalendarFlows, Principal.compare, caller);
let connection = await* LibCalendar.exchangeCode(
calendarConfig.clientId, calendarConfig.clientSecret, code,
pending.redirectUri, pending.codeVerifier,
);
Map.add(calendarConnections, Principal.compare, caller, connection);
};
public shared ({ caller }) func listUpcomingEvents(
timeMin : Text, maxResults : Nat,
) : async LibCalendar.EventSummaryList {
if (caller.isAnonymous()) {
Runtime.trap("Sign in to list events");
};
let ?connection = Map.get(calendarConnections, Principal.compare, caller) else {
Runtime.trap("Connect your Google Calendar first");
};
await* LibCalendar.listUpcomingEvents(
calendarConfig.clientId, calendarConfig.clientSecret, connection, caller,
calendarConnections, timeMin, maxResults,
);
};
public shared ({ caller }) func createEvent(
summary : Text, startDateTime : Text, endDateTime : Text,
) : async Text {
if (caller.isAnonymous()) {
Runtime.trap("Sign in to create events");
};
let ?connection = Map.get(calendarConnections, Principal.compare, caller) else {
Runtime.trap("Connect your Google Calendar first");
};
await* LibCalendar.createEvent(
calendarConfig.clientId, calendarConfig.clientSecret, connection, caller,
calendarConnections, summary, startDateTime, endDateTime,
);
};
public shared ({ caller }) func disconnectMyCalendar() : async () {
if (caller.isAnonymous()) {
Runtime.trap("Sign in to disconnect");
};
Map.remove(calendarConnections, Principal.compare, caller);
};
};import Array "mo:core/Array";
import Map "mo:core/Map";
import Nat64 "mo:core/Nat64";
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import Text "mo:core/Text";
import OAuth "mo:google-oauth/OAuth";
import { calendar_events_list; calendar_events_insert } "mo:googlecalendar-client/Apis/EventsApi";
import { type Event; JSON = Event } "mo:googlecalendar-client/Models/Event";
import { type EventDateTime; JSON = EventDateTime } "mo:googlecalendar-client/Models/EventDateTime";
import { type Events; JSON = Events } "mo:googlecalendar-client/Models/Events";
import { defaultConfig; type Config } "mo:googlecalendar-client/Config";
module {
public type CalendarConnection = {
accessToken : Text;
refreshToken : Text;
};
public type PendingOAuth = {
codeVerifier : Text;
redirectUri : Text;
state : Text;
};
public type EventSummary = {
id : Text;
summary : Text;
start : Text;
end : Text;
};
public type EventSummaryList = [EventSummary];
let SCOPES : Text = "https://www.googleapis.com/auth/calendar";
func configForToken(token : Text) : Config {
{
defaultConfig with
auth = ?#bearer(token);
is_replicated = ?false;
max_response_bytes = ?Nat64.fromNat(2_000_000);
};
};
func refreshIfNeeded(
clientId : Text, clientSecret : Text, connection : CalendarConnection,
caller : Principal, calendarConnections : Map.Map<Principal, CalendarConnection>,
errorMsg : Text,
) : async* ?Text {
if (not (errorMsg.contains(#text("401")) or errorMsg.contains(#text("Unauthorized")))) {
Runtime.trap("Calendar API failed: " # errorMsg);
};
let refreshed = await OAuth.refreshAccessToken(clientId, clientSecret, connection.refreshToken);
let newToken = accessTokenOf(refreshed, "Token refresh");
Map.add(calendarConnections, Principal.compare, caller, {
connection with accessToken = newToken;
});
?newToken;
};
public func startAuthorize(
clientId : Text, redirectUri : Text, caller : Principal,
pendingFlows : Map.Map<Principal, PendingOAuth>,
) : async* Text {
let codeVerifier = await OAuth.generateCodeVerifier();
let state = await OAuth.generateCodeVerifier();
Map.add(pendingFlows, Principal.compare, caller, {
codeVerifier;
redirectUri;
state;
});
OAuth.buildAuthorizeUrl(clientId, redirectUri, SCOPES, state, OAuth.computeCodeChallenge(codeVerifier));
};
public func exchangeCode(
clientId : Text, clientSecret : Text, code : Text,
redirectUri : Text, codeVerifier : Text,
) : async* CalendarConnection {
let tokens = await OAuth.exchangeAuthorizationCode(clientId, clientSecret, code, redirectUri, codeVerifier);
let accessToken = accessTokenOf(tokens, "Token exchange");
let refreshToken = switch (tokens.refreshToken) {
case (?t) t;
case null Runtime.trap("Token exchange failed: missing refresh_token");
};
{ accessToken; refreshToken };
};
func accessTokenOf(tokens : OAuth.TokenResponse, operation : Text) : Text {
switch (tokens.error) {
case (?error) {
let description = switch (tokens.errorDescription) {
case (?value) ": " # value;
case null "";
};
Runtime.trap(operation # " failed: " # error # description);
};
case null {};
};
switch (tokens.accessToken) {
case (?token) token;
case null Runtime.trap(operation # " failed: missing access_token");
};
};
public func listUpcomingEvents(
clientId : Text, clientSecret : Text, connection : CalendarConnection,
caller : Principal, calendarConnections : Map.Map<Principal, CalendarConnection>,
timeMin : Text, maxResults : Nat,
) : async* EventSummaryList {
if (timeMin.size() == 0) {
Runtime.trap("timeMin must be an RFC 3339 timestamp");
};
let events : Events = try {
await* calendar_events_list(
configForToken(connection.accessToken), "primary", #json,
"", "", "", true, "", "",
false, [], "", 0, maxResults, #starttime,
"", [], "", [], false, false, true, "",
"", timeMin, "", "",
);
} catch e {
let ?newToken = await* refreshIfNeeded(
clientId, clientSecret, connection, caller, calendarConnections, e.message(),
) else Runtime.trap("Calendar API failed");
await* calendar_events_list(
configForToken(newToken), "primary", #json,
"", "", "", true, "", "",
false, [], "", 0, maxResults, #starttime,
"", [], "", [], false, false, true, "",
"", timeMin, "", "",
);
};
eventSummariesOf(events);
};
public func createEvent(
clientId : Text, clientSecret : Text, connection : CalendarConnection,
caller : Principal, calendarConnections : Map.Map<Principal, CalendarConnection>,
summary : Text, startDateTime : Text, endDateTime : Text,
) : async* Text {
let start : EventDateTime = { EventDateTime.init {} with dateTime = ?startDateTime };
let end : EventDateTime = { EventDateTime.init {} with dateTime = ?endDateTime };
let event : Event = { Event.init {} with
summary = ?summary;
start = ?start;
end = ?end;
};
let created : Event = try {
await* calendar_events_insert(
configForToken(connection.accessToken), "primary", #json,
"", "", "", true, "", "",
0, 0, true, #all, false, event,
);
} catch e {
let ?newToken = await* refreshIfNeeded(
clientId, clientSecret, connection, caller, calendarConnections, e.message(),
) else Runtime.trap("Calendar API failed");
await* calendar_events_insert(
configForToken(newToken), "primary", #json,
"", "", "", true, "", "",
0, 0, true, #all, false, event,
);
};
switch (created.id) {
case (?id) id;
case null "";
};
};
func eventSummariesOf(events : Events) : EventSummaryList {
let items = switch (events.items) {
case (?items) items;
case null [];
};
Array.map<Event, EventSummary>(items, func(e : Event) : EventSummary = {
id = switch (e.id) { case (?id) id; case null "" };
summary = switch (e.summary) { case (?s) s; case null "(no title)" };
start = switch (e.start) {
case (?dt) switch (dt.dateTime) { case (?t) t; case null switch (dt.date) { case (?d) d; case null "" } };
case null "";
};
end = switch (e.end) {
case (?dt) switch (dt.dateTime) { case (?t) t; case null switch (dt.date) { case (?d) d; case null "" } };
case null "";
};
});
};
};google-oauth| Function | Purpose |
|---|---|
| RFC 3986 percent-encoding for form bodies |
| Parse Google token-endpoint JSON |
| Exchange auth code for tokens |
| Refresh an expired access token |
| Generate PKCE |
| Compute PKCE |
| Build the Google OAuth authorize URL |
googlecalendar-clientis_replicated = ?falserefreshIfNeeded| Function | Module | Purpose |
|---|---|---|
| EventsApi | List events on a calendar |
| EventsApi | Get an event by id |
| EventsApi | Create an event |
| EventsApi | Update an event (PUT) |
| EventsApi | Patch an event (PATCH) |
| EventsApi | Delete an event |
| EventsApi | Move an event to another calendar |
| EventsApi | Create event from text ("Lunch at noon") |
| EventsApi | List instances of a recurring event |
| FreebusyApi | Check free/busy across calendars |
| CalendarListApi | List user's calendars |
| CalendarListApi | Get a calendar list entry |
| CalendarsApi | Get calendar metadata |
| CalendarsApi | Create a secondary calendar |
google-oauthCall.httpRequestmo:ic/Callic0.cost_http_requestgooglecalendar-clientdefaultConfig.cycles = 30_000_000_000max_response_bytes = ?2_000_000is_replicated = ?falserefresh_tokenrefresh_tokenaccess_tokenrefreshIfNeededgoogle-oauth.refreshAccessTokenredirect_uri_mismatchwindow.location.origin + window.location.pathnameredirectUri2026-07-10T15:00:00-07:00EventDateTime.dateYYYY-MM-DDdateTimecalendarId = "primary"calendarConnectionsMap.get(calendarConnections, ..., caller)getMyCalendarConnectiongetMyAccessTokenalt = #json""prettyPrint = falseEventEventDateTimeinit {}?Tgooglecalendar-clientis_replicated = ?falseConfigcaller : Principal#adminextension-authorizationuseInternetIdentityuseActor/settings/calendarsetCalendarCredentials(clientId, clientSecret)isCalendarConfigured()Boolextension-authorizationisCallerAdmin/connect/calendarstartCalendarOAuth(window.location.origin + window.location.pathname)errorcodestateURLSearchParamserrorcodestatecompleteCalendarOAuth(code, state)history.replaceStateisMyCalendarConnected()BooldisconnectMyCalendar()timeMinlistUpcomingEvents(new Date().toISOString(), 10)singleEvents = trueorderBy = startTimedatetime-localcreateEvent(summary, new Date(startInput).toISOString(), new Date(endInput).toISOString())isMyCalendarConnected()false/connect/calendar/ → Main UI (upcoming events + create form)
/settings/calendar → Admin credential config (admin-only)
/connect/calendar → Per-user OAuth handshake (any signed-in user)/settings/.../connect/calendarextension-authorizationuseInternetIdentity!isAuthenticatedlocalStorageIndexedDBBoolstatecodestatecompleteCalendarOAuthmops add googlecalendar-client@0.1.3mops add google-oauth@0.1.4googlecalendar-clientuseInternetIdentityuseActor#admingoogle-oauth