Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Slack By James Joyner IV · · 10 min read

Slack Web API Pagination: Cursors, Limits, and Not Missing Data in Ops Bots

Master Slack Web API cursor pagination so your ops bot never silently drops members, messages, or channels. AI scaffolds the loop; you verify it's complete.

  • #slack
  • #chatops
  • #web-api
  • #reliability

The bug that taught me to respect Slack pagination was almost invisible. My audit bot listed channel members and posted “everyone in #prod-access reviewed.” It worked in dev, it worked in staging, and in production it quietly reported 100 members in a channel that had 340. The other 240 people simply weren’t in the first page of results, and my code never asked for page two. No error, no warning — just a confidently incomplete answer about who had access to production. For an audit tool, “silently incomplete” is the worst possible failure mode.

I write this kind of code with AI now, and pagination is a perfect example of why I treat it as a fast junior engineer. The model writes a clean-looking single call that “works” against a small workspace, and it will not notice that real data spans dozens of pages. You verify the loop terminates correctly and covers everything before this touches a real workspace. Fast draft, human-owned correctness.

How Slack cursor pagination works

Most list-style Web API methods (conversations.list, conversations.members, users.list, conversations.history) return a response_metadata.next_cursor. If it’s a non-empty string, there’s more data. You pass it back as the cursor param to get the next page:

{
  "ok": true,
  "members": ["U1", "U2", "U3"],
  "response_metadata": { "next_cursor": "dXNlcjpVMEc5V0ZYTlo=" }
}

When next_cursor is an empty string, you’ve reached the end. The single most common bug is treating no error as no more data. Empty cursor is the terminator, not response size.

The correct loop by hand

Here is the pattern, written explicitly so the termination condition is obvious:

async function allMembers(client, channel) {
  const members = [];
  let cursor;
  do {
    const res = await client.conversations.members({
      channel, limit: 200, cursor,
    });
    members.push(...res.members);
    cursor = res.response_metadata?.next_cursor || undefined;
  } while (cursor);
  return members;
}

The || undefined turns Slack’s empty-string terminator into a falsy value that exits the loop. Miss that and you either loop forever or stop early, depending on your condition.

Prefer the SDK’s paginate helper

The Bolt/Web API SDK has a paginate async iterator that handles cursors for you, which removes a whole class of off-by-one mistakes:

const members = [];
for await (const page of client.paginate('conversations.members', {
  channel, limit: 200,
})) {
  members.push(...page.members);
}

I default to this. The only reason to hand-roll the loop is when you need to stop early based on content (for example, stop scanning history once you pass a timestamp).

Pro Tip: limit is a maximum, not a guarantee. Slack may return fewer items than you asked for and still have more pages. Never use “returned fewer than limit” as your stop condition — only the empty cursor means done.

Pagination and rate limits are the same problem

Every page is an API call, and list methods sit in rate-limit tiers. A naive paginate over a huge users.list will hit 429 fast. The SDK respects Retry-After automatically, but if you’re hand-rolling, you must too:

async function withRetry(fn) {
  for (let attempt = 0; ; attempt++) {
    try { return await fn(); }
    catch (e) {
      if (e.code === 'slack_webapi_platform_error' && e.data?.error === 'ratelimited') {
        const wait = (Number(e.retryAfter) || 2 ** attempt) * 1000;
        await new Promise(r => setTimeout(r, wait));
        continue;
      }
      throw e;
    }
  }
}

If you want the full treatment on backoff and batching, that’s its own discipline — but the headline is: deep pagination and rate limits are inseparable.

Cursors expire — design for restarts

Cursors are not stable forever. If your bot crashes mid-walk and you stashed a cursor to resume hours later, it may be invalid. For long-running scans (a full workspace member export), make the operation restartable from the start and idempotent on output, rather than assuming you can pause and resume on a saved cursor. For audit work especially, “I’ll just resume from cursor X” is a trap when the underlying data has shifted.

Where AI helps, where you stay in charge

The model is genuinely good at writing the paginate loop, wiring in retry, and converting a list result into the shape your tool needs. I let Claude or Copilot draft it and refine the prompt in the prompt workspace. But for anything where completeness is a correctness property — access audits feeding your code review or compliance pipeline — a human verifies the loop actually walks every page before you trust its output. And as always, don’t hand the model a real bot token to “try it live”; give it the API shape and review the diff.

Conclusion

Slack pagination fails silently, which makes it dangerous precisely in the tools you most need to trust. Walk every page until the cursor is empty, prefer the SDK iterator, fold in rate-limit retry, and design long scans to restart cleanly. Let AI write the loop fast; you confirm it never drops a row. More in the Slack category, and reusable prompts to scaffold the boilerplate.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.