Agent Skills: ClickUp Cost Tuning

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/clickup-cost-tuning

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/clickup-pack/skills/clickup-cost-tuning

Skill Files

Browse the full folder contents for clickup-cost-tuning.

Download Skill

Loading file tree…

plugins/saas-packs/clickup-pack/skills/clickup-cost-tuning/SKILL.md

Skill Metadata

Name
clickup-cost-tuning
Description
|

ClickUp Cost Tuning

Overview

ClickUp charges per-seat, not per-API-call. However, rate limits constrain throughput per plan tier. Optimizing API usage means reducing request count to stay within rate limits and avoid needing plan upgrades.

ClickUp Pricing (Per Member/Month)

| Plan | Price | Rate Limit | Key API Features | |------|-------|-----------|------------------| | Free Forever | $0 | 100 req/min | Full API access, 100 uses of automations | | Unlimited | $7/member | 100 req/min | Unlimited storage, integrations | | Business | $12/member | 100 req/min | Custom fields, time tracking, goals | | Business Plus | $19/member | 1,000 req/min | Custom role creation, admin training | | Enterprise | Custom | 10,000 req/min | SSO/SAML, advanced permissions, dedicated support |

Request Reduction Strategies

1. Cache Workspace Structure

Spaces, folders, and lists change rarely. Cache them aggressively.

import { LRUCache } from 'lru-cache';

const structureCache = new LRUCache<string, any>({
  max: 500,
  ttl: 300000, // 5 minutes for hierarchy data
});

async function getCachedSpaces(teamId: string) {
  const key = `spaces:${teamId}`;
  let spaces = structureCache.get(key);
  if (!spaces) {
    const data = await clickupRequest(`/team/${teamId}/space?archived=false`);
    spaces = data.spaces;
    structureCache.set(key, spaces);
  }
  return spaces;
}

2. Use Pagination Efficiently

Get Tasks returns max 100 per page. Fetch only what you need.

// Bad: fetch all pages when you only need recent tasks
// Good: use filters to minimize pages
async function getRecentTasks(listId: string, limit = 25) {
  return clickupRequest(`/list/${listId}/task?${new URLSearchParams({
    page: '0',
    order_by: 'updated',
    reverse: 'true',
    subtasks: 'true',
    include_closed: 'false',
  })}`);
}

3. Batch with Custom Fields

Set custom fields during task creation instead of separate calls.

// Bad: 3 API calls (create + 2 custom field updates)
const task = await createTask(listId, { name: 'Task' });
await setCustomField(task.id, field1Id, value1);
await setCustomField(task.id, field2Id, value2);

// Good: 1 API call (custom fields in create body)
await createTask(listId, {
  name: 'Task',
  custom_fields: [
    { id: field1Id, value: value1 },
    { id: field2Id, value: value2 },
  ],
});

4. Use Webhooks Instead of Polling

// Bad: poll every 30 seconds (2 req/min wasted)
setInterval(() => checkForUpdates(), 30000);

// Good: register webhook, process events on-demand (0 polling requests)
await clickupRequest(`/team/${teamId}/webhook`, {
  method: 'POST',
  body: JSON.stringify({
    endpoint: 'https://myapp.com/webhooks/clickup',
    events: ['taskUpdated', 'taskCreated'],
  }),
});

Usage Monitoring

class ClickUpUsageTracker {
  private requestLog: Array<{ timestamp: number; endpoint: string }> = [];

  track(endpoint: string): void {
    this.requestLog.push({ timestamp: Date.now(), endpoint });

    // Keep only last hour
    const cutoff = Date.now() - 3600000;
    this.requestLog = this.requestLog.filter(r => r.timestamp > cutoff);
  }

  getRequestsPerMinute(): number {
    const oneMinAgo = Date.now() - 60000;
    return this.requestLog.filter(r => r.timestamp > oneMinAgo).length;
  }

  getTopEndpoints(n = 5): Array<{ endpoint: string; count: number }> {
    const counts = new Map<string, number>();
    for (const r of this.requestLog) {
      counts.set(r.endpoint, (counts.get(r.endpoint) ?? 0) + 1);
    }
    return [...counts.entries()]
      .sort((a, b) => b[1] - a[1])
      .slice(0, n)
      .map(([endpoint, count]) => ({ endpoint, count }));
  }

  needsUpgrade(): boolean {
    return this.getRequestsPerMinute() > 80; // 80% of Free tier limit
  }
}

Cost Decision Matrix

| Monthly Requests | Recommended Plan | Rationale | |-----------------|-----------------|-----------| | < 144,000 | Free Forever | 100/min * 60min * 24h = 144K/day max | | 100-1000 req/min sustained | Business Plus | 10x rate limit increase | | > 1000 req/min sustained | Enterprise | 10,000 req/min + dedicated support |

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | Constant 429 errors | Hit rate ceiling | Implement queuing or upgrade | | Cache stale data | TTL too long | Invalidate via webhooks | | Redundant API calls | No deduplication | Use DataLoader batching | | Polling overhead | No webhook setup | Switch to event-driven |

Resources

Next Steps

For architecture patterns, see clickup-reference-architecture.