Skip to the content.

AI Agents Integration

Use the Skool All-in-One API actor as a tool for AI agents. Includes function-calling specs (Claude / OpenAI / Gemini), MCP server pattern, LangChain Tool definition, Claude Code Skill, and example agent loops.

The actor’s response shape is deliberately agent-friendly: structured {success, errorCode, hint} payloads let an LLM self-correct without wrapper logic. If a call fails, the hint field tells the model what to do next.

Quick comparison

Stack Method Where to plug it
Claude tool use (function calling) Function definition Anthropic Messages API tools
OpenAI function calling Tool definition Chat Completions / Assistants API tools
Gemini function calling Function declaration generateContent tools
LangChain Tool class Add to your agent’s tool list
Claude Code Skill .claude/skills/skool-actor/SKILL.md (example)
MCP server Wrap as MCP tool Self-hosted MCP server proxying to Apify
OpenClaw / Nyx Skill OpenClaw skill format

Pattern: one tool per action

The cleanest pattern is to expose one tool per actor action: skool_posts_create, skool_members_approve, skool_classroom_create_course, etc. The LLM picks the right one. Don’t try to wrap everything in a single skool_call(action, params) tool — LLMs route worse without per-action schemas.

Anthropic Claude — tool definitions

{
  "name": "skool_members_pending",
  "description": "List Skool community members awaiting approval. Returns each member with id, memberId, firstName, lastName, email, bio, country, linkedinUrl, applicationAnswer. Use memberId (not id) for subsequent approve/reject calls.",
  "input_schema": {
    "type": "object",
    "properties": {
      "groupSlug": { "type": "string", "description": "The community slug (skool.com/{slug})" },
      "limit": { "type": "number", "description": "Max members to return", "default": 50 }
    },
    "required": ["groupSlug"]
  }
}
{
  "name": "skool_members_approve",
  "description": "Approve a pending Skool member. Pass the member's memberId (not the request id). Idempotent: re-approving an already-approved member returns 'cannot update to same role' which is a no-op.",
  "input_schema": {
    "type": "object",
    "properties": {
      "groupSlug": { "type": "string" },
      "memberId": { "type": "string", "description": "32-char hex member ID from members:pending response" }
    },
    "required": ["groupSlug", "memberId"]
  }
}
{
  "name": "skool_posts_create_comment",
  "description": "Create a comment on a Skool post or a nested reply to a comment. For top-level comments: rootId equals parentId equals postId. For nested replies: rootId equals postId, parentId equals commentId. Content is plain text only — no HTML or markdown rendering.",
  "input_schema": {
    "type": "object",
    "properties": {
      "groupSlug": { "type": "string" },
      "rootId": { "type": "string", "description": "Always the original post's id" },
      "parentId": { "type": "string", "description": "postId for top-level comments, or commentId for nested replies" },
      "content": { "type": "string", "description": "Plain text. Use [@Name](obj://user/{userId}) for mentions." }
    },
    "required": ["groupSlug", "rootId", "parentId", "content"]
  }
}

Generate similar definitions for every action you want exposed. The naming convention skool_<namespace>_<operation> keeps the model’s tool list scannable.

In your handler, when the model calls a tool, translate to an Apify run:

async function handleToolCall(toolName, toolInput, cookies, apifyToken) {
  // Map "skool_members_approve" → action "members:approve"
  const action = toolName.replace(/^skool_/, '').replace(/_/g, ':');
  const r = await fetch(
    `https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token=${apifyToken}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        action,
        cookies,
        groupSlug: toolInput.groupSlug,
        params: { ...toolInput, groupSlug: undefined },
      }),
    },
  );
  const items = await r.json();
  return items[0]; // single-action run = single dataset item
}

OpenAI / Gemini — almost identical

OpenAI’s tools array uses function.parameters (same JSON Schema). Gemini’s function_declarations use parameters. The shapes above translate 1:1.

LangChain example

from langchain.agents import Tool
import requests

APIFY_TOKEN = "..."
GROUP_SLUG = "your-community"
COOKIES = "..."  # from auth:login

def call_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}",
        json={"action": action, "cookies": COOKIES, "groupSlug": GROUP_SLUG, "params": params},
    )
    return r.json()[0]

tools = [
    Tool(
        name="skool_members_pending",
        description="List members awaiting approval. Returns memberId for each.",
        func=lambda _: call_actor("members:pending", {"limit": 50}),
    ),
    Tool(
        name="skool_members_approve",
        description="Approve a member. Input: memberId (32-char hex).",
        func=lambda memberId: call_actor("members:approve", {"memberId": memberId}),
    ),
    # ...
]

Claude Code Skill

If you use Claude Code, drop the skool-actor skill into .claude/skills/skool-actor/:

.claude/skills/skool-actor/
├── SKILL.md       ← invocation triggers + reference table + auth flow
└── scripts/
    ├── login.sh
    ├── post.sh
    ├── comment.sh
    └── approve.sh

The minimal SKILL.md is in skills/claude-code/skool-actor/ of this repo. Copy that folder into your .claude/skills/ and Claude Code will surface it whenever you say things like “list pending Skool members” or “publish a course from this markdown.”

MCP (Model Context Protocol) server pattern

If you want the actor accessible to any MCP-aware client (Claude Desktop, Cursor, Cline, etc.), wrap it as a self-hosted MCP server:

// server.js — minimal MCP server proxying to Apify
import { Server } from '@modelcontextprotocol/sdk/server';

const server = new Server({ name: 'skool-actor', version: '0.1.0' }, {
  capabilities: { tools: {} },
});

server.setRequestHandler('tools/list', async () => ({
  tools: [
    {
      name: 'skool_members_pending',
      description: 'List Skool members awaiting approval',
      inputSchema: { /* JSON Schema */ },
    },
    // ... more tools
  ],
}));

server.setRequestHandler('tools/call', async (req) => {
  const action = req.params.name.replace(/^skool_/, '').replace(/_/g, ':');
  const result = await callActor(action, req.params.arguments);
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
});

server.start();

Then in Claude Desktop’s claude_desktop_config.json:

{
  "mcpServers": {
    "skool": {
      "command": "node",
      "args": ["/path/to/server.js"],
      "env": {
        "APIFY_TOKEN": "...",
        "SKOOL_COOKIES": "...",
        "SKOOL_GROUP_SLUG": "..."
      }
    }
  }
}

The Skool community + Apify reach gets a free integration with every MCP-aware tool.

Agent loop pattern: auto-respond to comments

Pseudo-code for an agent that monitors new comments and replies on-brand:

// 1. Every N minutes:
const posts = await callActor('posts:list', { limit: 5 });
for (const post of posts.posts) {
  const { comments } = await callActor('posts:getComments', { postId: post.id });
  for (const c of comments) {
    if (c.author.id === selfUserId) continue;          // skip own comments
    if (await alreadyReplied(c.id)) continue;          // skip already-replied
    if (post.commentCount > THRESHOLD) continue;       // skip already-active threads

    // 2. Ask LLM to draft a reply
    const draft = await llmDraftReply({
      postContent: post.content,
      commentContent: c.content,
      brandVoice: brandVoiceMd,
    });

    // 3. Human approval (or auto-publish for low-stakes)
    if (await humanApprove(draft)) {
      await callActor('posts:createComment', {
        rootId: post.id,
        parentId: c.id,
        content: draft,
      });
    }
  }
}

This is the pattern running for Cágala, Aprende, Repite — checks daily, drafts via Claude, requires Telegram approval before publishing.

Mistakes to avoid with agents

❌ Don’t expose auth:login as a tool

auth:login returns secrets (cookies). If the LLM thinks it’s a routine tool call, it might paste the cookies into a downstream message. Run auth:login once per ~3 days from a privileged side process, store cookies in a secret manager, and pass cookies to the agent’s tool handler server-side — never as part of the tool call’s input.

❌ Don’t trust the LLM with members:ban

Banning is irreversible-feeling (member can’t re-apply). Hold human approval before any members:ban — even if the LLM is “sure.”

❌ Don’t loop on RATE_LIMIT without backoff

If the model retries immediately after RATE_LIMIT, it’ll just get the same error. Wrap your tool handler so it sleeps 60s before retry, then returns success/failure. Don’t let the LLM see and self-retry.

❌ Don’t expose classroom:deleteUnit casually

Cascading delete. Consider gating destructive actions behind a confirmation step.

See also