Skip to the content.

Quick reference (TL;DR for agents)

The Tool — minimal

from langchain.tools import StructuredTool
from pydantic import BaseModel, Field
import requests, os

class SkoolActionInput(BaseModel):
    action: str = Field(description="Skool action, e.g. 'posts:create', 'members:approve'. See https://github.com/ctala/skool-api-docs/blob/main/docs/actions.md")
    params: dict = Field(default_factory=dict, description="Action-specific params")

def skool_action(action: str, params: dict) -> dict:
    """Call the Apify-hosted Skool actor. Returns structured success/failure."""
    resp = requests.post(
        f"https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items"
        f"?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
    )
    resp.raise_for_status()
    data = resp.json()
    return data[0] if isinstance(data, list) and data else data

skool_tool = StructuredTool.from_function(
    func=skool_action,
    name="skool_action",
    description=(
        "Perform a read or write operation on a Skool community. "
        "Returns {success: true, data: ...} or {success: false, errorCode, hint}. "
        "On failure, read the 'hint' field — it tells you what to do next. "
        "For comment replies: rootId == postId, parentId == commentId (NOT postId). "
        "For member operations: use memberId (from members:pending), NOT user id."
    ),
    args_schema=SkoolActionInput,
)

Using in an Agent

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_anthropic import ChatAnthropic
from langchain.prompts import ChatPromptTemplate

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

prompt = ChatPromptTemplate.from_messages([
    ("system", "You operate a Skool community via the skool_action tool. Be precise with params."),
    ("user", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(llm, [skool_tool], prompt)
executor = AgentExecutor(agent=agent, tools=[skool_tool], verbose=True, max_iterations=10)

result = executor.invoke({
    "input": "Approve the latest 3 pending members. Reply to me with a one-line summary of each."
})
print(result["output"])

Async version

import httpx
from langchain.tools import StructuredTool

async def skool_action_async(action: str, params: dict) -> dict:
    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.post(
            f"https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items"
            f"?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 {}}
        )
        data = resp.json()
        return data[0] if isinstance(data, list) and data else data

skool_tool_async = StructuredTool.from_function(
    func=skool_action,             # sync fallback
    coroutine=skool_action_async,  # async path
    name="skool_action",
    description="...",
    args_schema=SkoolActionInput,
)

LangChain’s agent executor calls the async version automatically when running with ainvoke.

Per-action tools (alternative pattern)

Instead of one generic skool_action tool, expose each action as a separate Tool. Slightly more tokens used per agent invocation (more tool schemas) but the LLM has higher confidence picking the right one.

def make_action_tool(action: str, description: str, params_schema):
    def fn(**kwargs) -> dict:
        return skool_action(action, kwargs)
    return StructuredTool.from_function(
        func=fn,
        name=action.replace(":", "_"),
        description=description,
        args_schema=params_schema,
    )

class PostsCreateInput(BaseModel):
    title: str
    content: str
    labelId: str | None = None

posts_create_tool = make_action_tool(
    "posts:create",
    "Create a new top-level post in the Skool feed. Plain text only (no HTML/markdown).",
    PostsCreateInput,
)

# ... repeat for ~25 actions

Use when your agent’s primary use case is a small subset of actions (e.g. just member approval). Skip when the agent needs the full surface.

Auto-rotate cookies on WAF_EXPIRED

Wrap the base call:

def skool_action_with_retry(action: str, params: dict) -> dict:
    result = skool_action(action, params)
    if not result.get("success") and result.get("errorCode") == "WAF_EXPIRED":
        # Rotate cookies
        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 = StructuredTool.from_function(func=skool_action_with_retry, ...)

Now the agent doesn’t see WAF_EXPIRED errors — the wrapper handles them transparently.

LangGraph pattern

For more complex agent state machines, use LangGraph:

from langgraph.graph import StateGraph, END
from typing import TypedDict, list

class AgentState(TypedDict):
    messages: list
    last_skool_result: dict

def agent_node(state):
    response = llm.invoke(state["messages"])
    state["messages"].append(response)
    return state

def tool_node(state):
    last = state["messages"][-1]
    if last.tool_calls:
        for tc in last.tool_calls:
            result = skool_action(tc.args["action"], tc.args["params"])
            state["messages"].append({"role": "tool", "content": str(result), "tool_call_id": tc.id})
            state["last_skool_result"] = result
    return state

def should_continue(state):
    last = state["messages"][-1]
    return "tool" if last.tool_calls else END

workflow = StateGraph(AgentState)
workflow.add_node("agent", agent_node)
workflow.add_node("tool", tool_node)
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue, {"tool": "tool", END: END})
workflow.add_edge("tool", "agent")
graph = workflow.compile()

Add Skool to your LangChain agent today

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

One StructuredTool wraps the entire Skool admin surface. Works with Claude, GPT, Gemini, local models. Pay-per-event (~$0.005-$0.01 per call).

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