Skip to the content.

Quick reference (TL;DR for agents)

Why LlamaIndex + Skool?

Skool has no official API. LlamaIndex is great at retrieval and at calling tools, but there’s no built-in connector to act on a Skool community — your agent can reason about Skool data only if something fetches and writes it for real.

The Skool All-in-One API actor turns every Skool admin action into one HTTP POST with a structured { success, data } / { success, errorCode, hint } response. That’s exactly the shape FunctionTool expects: define one Python function, hand it to FunctionTool.from_defaults, and your FunctionAgent can read and write Skool. One tool, the whole surface — no SDK, no Playwright in your runtime.

Why LlamaIndex specifically fits well here:

  1. FunctionTool is the lightest tool primitive going. A single typed function with a docstring becomes a callable tool — no schema boilerplate.
  2. Pairs with retrieval. Index your community’s posts or course content as a query engine and give the agent the Skool write tool — read context, then act.
  3. FunctionAgent runs the loop for you. Plan → call tool → read result → decide, with the actor’s LLM-readable hint driving recovery.

The tool — FunctionTool

Wrap the actor in one function and register it. Set APIFY_TOKEN, SKOOL_COOKIES, and SKOOL_GROUP_SLUG in your environment first.

import os
import requests
from llama_index.core.tools import FunctionTool

def skool_action(action: str, params: dict | None = None) -> dict:
    """Perform a read or write action on a Skool community via the Apify-hosted
    Skool All-in-One API actor.

    action: e.g. 'posts:create', 'members:pending', 'members:approve'.
            See https://skool-api.cristiantala.com/docs/actions/
    params: action-specific dict. members approve/reject use memberId (NOT id).
            Comment reply: parentId = comment id, rootId = post id.

    Returns {success: true, data: ...} or {success: false, errorCode, hint}.
    On failure, read the 'hint' field — it tells you what to do next.
    """
    token = os.environ["APIFY_TOKEN"]
    url = (
        "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api"
        "/run-sync-get-dataset-items"
        f"?token={token}&build=latest&timeout=90"
    )
    resp = requests.post(
        url,
        json={
            "action": action,
            "cookies": os.environ["SKOOL_COOKIES"],
            "groupSlug": os.environ["SKOOL_GROUP_SLUG"],
            "params": params or {},
        },
        timeout=120,
    )
    resp.raise_for_status()
    data = resp.json()
    # the actor returns a list; the first item is the result
    return data[0] if isinstance(data, list) and data else data

skool_tool = FunctionTool.from_defaults(
    fn=skool_action,
    name="skool_action",
    description=(
        "Perform a Skool action via the Apify-hosted actor. Returns structured "
        "{success, data} or {success: false, errorCode, hint}. members approve/reject "
        "use memberId (NOT id). Comment reply: parentId = comment id, rootId = post id."
    ),
)

Example — a community-manager agent

A single FunctionAgent that clears the waitlist and posts a welcome.

import asyncio
from llama_index.llms.anthropic import Anthropic
from llama_index.core.agent.workflow import FunctionAgent

llm = Anthropic(model="claude-opus-4-7", api_key=os.environ["ANTHROPIC_API_KEY"])

agent = FunctionAgent(
    tools=[skool_tool],
    llm=llm,
    system_prompt=(
        "You operate a Skool community via the skool_action tool. "
        "Approve only applicants with a reachable LinkedIn and a specific survey answer. "
        "Use memberId (from members:pending), never id. Posts are plain text."
    ),
)

async def main():
    response = await agent.run(
        "List my pending Skool members. Approve the ones with a real LinkedIn using "
        "members:batchApprove. Then post a 2-sentence welcome naming them. "
        "Give me a one-line summary of who you approved and who you left for review."
    )
    print(response)

asyncio.run(main())

The agent calls members:pending, screens the queue, batch-approves the clear yeses, then posts:creates the welcome — typically under $0.05 for a full waitlist clear. The task pattern is detailed in Review & batch-approve your waitlist.

Prefer a reasoning-trace agent? ReActAgent takes the same tool list:

from llama_index.core.agent.workflow import ReActAgent

agent = ReActAgent(tools=[skool_tool], llm=llm)

Auto-rotate cookies on WAF_EXPIRED

Skool cookies age out about every 3.5 days. Wrap the call so the agent never sees the error:

def skool_action_safe(action: str, params: dict | None = None) -> dict:
    result = skool_action(action, params)
    if not result.get("success") and result.get("errorCode") == "WAF_EXPIRED":
        login = skool_action("auth:login", {
            "email": os.environ["SKOOL_EMAIL"],
            "password": os.environ["SKOOL_PASSWORD"],
            "groupSlug": os.environ["SKOOL_GROUP_SLUG"],
        })
        if login.get("success"):
            os.environ["SKOOL_COOKIES"] = login["cookies"]
            return skool_action(action, params)
    return result

skool_tool = FunctionTool.from_defaults(fn=skool_action_safe, name="skool_action")

Register skool_action_safe instead of skool_action and re-logins happen transparently.

Production gotchas

Hand this to your agent

Already have a tool-calling loop? Drop in the function body and give the LLM this primer — full first available at Skool for AI agents:

def skool_action(action: str, params: dict | None = None) -> dict:
    import os, requests
    r = requests.post(
        "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api"
        f"/run-sync-get-dataset-items?token={os.environ['APIFY_TOKEN']}&build=latest&timeout=90",
        json={"action": action, "cookies": os.environ["SKOOL_COOKIES"],
              "groupSlug": os.environ["SKOOL_GROUP_SLUG"], "params": params or {}},
        timeout=120,
    )
    d = r.json()
    return d[0] if isinstance(d, list) and d else d
Skool has no official API; the Apify actor cristiantala/skool-all-in-one-api is it.
Every action is ONE POST. Actions: posts:* members:* events:* classroom:* files:* groups:* auth:login.
Rules: members approve/reject use params.memberId (NOT id). Comment reply: parentId=comment id,
rootId=post id. Posts are plain text. ~25 writes/min (actor queues — no retry loop). On
errorCode "WAF_EXPIRED" re-run auth:login. Read `hint` to recover. Full params:
https://skool-api.cristiantala.com/docs/actions/

Other Python frameworks

Using a different framework? The wrapper is the same POST behind a different tool interface:

See also


Plug Skool into your LlamaIndex agent today

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

No Skool community yet? Launch one in 10 minutes — 14-day free trial.