Skip to the content.

Quick reference (TL;DR for agents)

What this recipe does

Skool’s calendar feature is great, but members don’t check the calendar tab — they check the feed. If you schedule a weekly community call and only put it in the calendar, you’ll see 30-50% lower attendance vs. announcing it in the feed every week.

This recipe runs on a schedule (e.g. every 6 hours), fetches upcoming events from events:upcoming, and:

  1. Posts a reminder in the community feed 24h before each event
  2. Sends a Telegram / Slack / WhatsApp ping 1h before (optional)
  3. Logs sent reminders to avoid duplicates

Used in production for weekly Cafecito calls — bumped attendance from ~12 to ~25 on a 500-member community.

Prerequisites

Step 1 — Get upcoming events

The events:upcoming action returns events sorted ascending. Each item has nextOccurrence, timezone, occurrenceId, and the event’s title + description.

{
  "action": "events:upcoming",
  "cookies": "{COOKIES}",
  "groupSlug": "your-community",
  "params": { "limit": 10 }
}

Response shape (simplified):

{
  "success": true,
  "data": {
    "events": [
      {
        "id": "abc123...",
        "occurrenceId": "abc123:1716220800",
        "name": "weekly-cafecito",
        "title": "Cafecito Startup — Wed 7pm",
        "description": "Weekly community call. Bring your stuck...",
        "nextOccurrence": "2026-05-21T22:00:00Z",
        "timezone": "America/Santiago",
        "url": "https://meet.google.com/abc-defg-hij"
      }
    ]
  }
}

Step 2 — Filter to “events in the next 24h or 1h”

In your scheduler (n8n Function node, Python script, etc.):

from datetime import datetime, timezone, timedelta

now = datetime.now(timezone.utc)
window_24h = now + timedelta(hours=24)
window_1h = now + timedelta(hours=1)

for ev in upcoming_events:
    occ = datetime.fromisoformat(ev["nextOccurrence"].replace("Z", "+00:00"))
    delta = occ - now

    if timedelta(hours=0) < delta <= timedelta(hours=1):
        send_1h_ping(ev)
    elif timedelta(hours=23) < delta <= timedelta(hours=25):
        post_24h_reminder(ev)

The delta bounds (23-25h for the 24h window, 0-1h for the 1h ping) give you slack if your scheduler runs every 6h or every hour and an event falls on the boundary.

Step 3 — Dedupe (avoid double announcements)

Track which occurrenceIds you’ve already announced. Simplest implementations:

import json
from pathlib import Path

state_file = Path("/tmp/skool-announced.json")
state = json.loads(state_file.read_text()) if state_file.exists() else {"announced": []}

def already_announced(occurrence_id, window):
    key = f"{occurrence_id}:{window}"
    return key in state["announced"]

def mark_announced(occurrence_id, window):
    key = f"{occurrence_id}:{window}"
    state["announced"].append(key)
    state_file.write_text(json.dumps(state))

Without dedupe, an hourly scheduler running through a 1h window will announce the same event multiple times.

Step 4 — Post the 24h reminder to the feed

import requests, os

def post_24h_reminder(ev):
    if already_announced(ev["occurrenceId"], "24h"):
        return

    when_local = format_local_time(ev["nextOccurrence"], ev.get("timezone"))
    body = (
        f"⏰ Recordatorio: {ev['title']} mañana — {when_local}.\n\n"
        f"{ev.get('description', '')[:200]}\n\n"
        f"Link directo a la sala: {ev.get('url', 'ver calendario')}"
    )

    payload = {
        "action": "posts:create",
        "cookies": os.environ["SKOOL_COOKIES"],
        "groupSlug": os.environ["SKOOL_GROUP_SLUG"],
        "params": {
            "title": f"Recordatorio: {ev['title'][:40]}",
            "content": body,
        }
    }
    r = requests.post(
        f"https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token={os.environ['APIFY_TOKEN']}&build=latest&timeout=60",
        json=payload, timeout=90
    )
    result = r.json()[0]
    if result.get("success"):
        mark_announced(ev["occurrenceId"], "24h")

Step 5 — Send the 1h ping to Telegram / Slack

For off-platform pings (most members get Telegram/Slack notifications faster than Skool push):

def send_1h_ping(ev):
    if already_announced(ev["occurrenceId"], "1h"):
        return

    text = f"🔴 EN VIVO EN 1 HORA: {ev['title']}\nLink: {ev.get('url', '')}"

    # Telegram
    requests.post(
        f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
        json={"chat_id": TELEGRAM_CHAT_ID, "text": text}
    )

    # Slack
    requests.post(SLACK_WEBHOOK_URL, json={"text": text})

    mark_announced(ev["occurrenceId"], "1h")

Combine with the WhatsApp Cloud API for full coverage if you have a VIP group there too.

Step 6 — Schedule the runner

GitHub Actions (free for personal use)

.github/workflows/skool-events.yml:

name: Skool event announcer
on:
  schedule:
    - cron: "0 */6 * * *"   # every 6 hours
  workflow_dispatch:

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: {python-version: "3.11"}
      - run: pip install requests
      - run: python events_bot.py
        env:
          APIFY_TOKEN: # secrets.APIFY_TOKEN
          SKOOL_COOKIES: # secrets.SKOOL_COOKIES
          SKOOL_GROUP_SLUG: # secrets.SKOOL_GROUP_SLUG
          TELEGRAM_TOKEN: # secrets.TELEGRAM_TOKEN
          TELEGRAM_CHAT_ID: # secrets.TELEGRAM_CHAT_ID

Linux cron

# every hour
0 * * * * cd /opt/skool-events && /usr/bin/python3 events_bot.py >> bot.log 2>&1

Production gotchas

Full Python script

events_bot.py — single-file implementation ```python #!/usr/bin/env python3 """Skool event announcer — runs on a schedule, posts 24h + 1h reminders.""" import os, json, requests from datetime import datetime, timezone, timedelta from pathlib import Path APIFY_TOKEN = os.environ["APIFY_TOKEN"] COOKIES = os.environ["SKOOL_COOKIES"] GROUP = os.environ["SKOOL_GROUP_SLUG"] TG_TOKEN = os.environ.get("TELEGRAM_TOKEN") TG_CHAT = os.environ.get("TELEGRAM_CHAT_ID") state_file = Path("/tmp/skool-announced.json") state = json.loads(state_file.read_text()) if state_file.exists() else {"announced": []} def actor(action, params): r = requests.post( f"https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token={APIFY_TOKEN}&build=latest&timeout=60", json={"action": action, "cookies": COOKIES, "groupSlug": GROUP, "params": params}, timeout=90 ) return r.json()[0] def announced(occ_id, win): return f"{occ_id}:{win}" in state["announced"] def mark(occ_id, win): state["announced"].append(f"{occ_id}:{win}") state_file.write_text(json.dumps(state)) events = actor("events:upcoming", {"limit": 10}).get("data", {}).get("events", []) now = datetime.now(timezone.utc) for ev in events: occ = datetime.fromisoformat(ev["nextOccurrence"].replace("Z", "+00:00")) delta_h = (occ - now).total_seconds() / 3600 if 0 < delta_h <= 1 and not announced(ev["occurrenceId"], "1h"): if TG_TOKEN: requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage", json={"chat_id": TG_CHAT, "text": f"🔴 EN VIVO EN 1H: {ev['title']}\n{ev.get('url','')}"}) mark(ev["occurrenceId"], "1h") elif 23 < delta_h <= 25 and not announced(ev["occurrenceId"], "24h"): body = f"⏰ Mañana: {ev['title']}\n\n{ev.get('description','')[:200]}\n\nLink: {ev.get('url', 'ver calendario')}" res = actor("posts:create", { "title": f"Recordatorio: {ev['title'][:40]}", "content": body }) if res.get("success"): mark(ev["occurrenceId"], "24h") print(f"Processed {len(events)} upcoming events. State: {len(state['announced'])} announcements logged.") ```

See also


Use it in production today

→ Open the Skool All-in-One API actor on Apify

Pay-per-event (~$0.02 per announcement). Battle-tested in production.

No Skool community yet? Launch one in 10 minutes — 14-day free trial. Need an n8n instance? Get started free — the workflow tool we use throughout these recipes.