agentsclimarketplace

Clickup sdk patterns

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/clickup-pack/skills/clickup-sdk-patterns

'Production-ready ClickUp API v2 client patterns with typed wrappers,From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill clickup-sdk-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.0 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it

ClickUp SDK Patterns

Overview

ClickUp has no official SDK. Build a typed REST client wrapper around https://api.clickup.com/api/v2/. These patterns provide singleton clients, typed responses, error boundaries, and multi-tenant support.

Typed Client Wrapper

// src/clickup/client.ts
const CLICKUP_BASE = 'https://api.clickup.com/api/v2';

interface ClickUpClientConfig {
  token: string;
  timeout?: number;
  onRateLimit?: (waitMs: number) => void;
}

class ClickUpClient {
  private token: string;
  private timeout: number;
  private rateLimitRemaining = 100;
  private rateLimitReset = 0;

  constructor(config: ClickUpClientConfig) {
    this.token = config.token;
    this.timeout = config.timeout ?? 30000;
  }

  async request<T>(path: string, options: RequestInit = {}): Promise<T> {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(`${CLICKUP_BASE}${path}`, {
        ...options,
        signal: controller.signal,
        headers: {
          'Authorization': this.token,
          'Content-Type': 'application/json',
          ...options.headers,
        },
      });

      // Track rate limit state from response headers
      this.rateLimitRemaining = parseInt(
        response.headers.get('X-RateLimit-Remaining') ?? '100'
      );
      this.rateLimitReset = parseInt(
        response.headers.get('X-RateLimit-Reset') ?? '0'
      ) * 1000;

      if (!response.ok) {
        const body = await response.json().catch(() => ({}));
        throw new ClickUpApiError(response.status, body.err, body.ECODE);
      }

      return response.json();
    } finally {
      clearTimeout(timer);
    }
  }

  // Convenience methods
  async getUser(): Promise<ClickUpUser> {
    const data = await this.request<{ user: ClickUpUser }>('/user');
    return data.user;
  }

  async getTeams(): Promise<ClickUpTeam[]> {
    const data = await this.request<{ teams: ClickUpTeam[] }>('/team');
    return data.teams;
  }

  async getSpaces(teamId: string): Promise<ClickUpSpace[]> {
    const data = await this.request<{ spaces: ClickUpSpace[] }>(
      `/team/${teamId}/space?archived=false`
    );
    return data.spaces;
  }

  async createTask(listId: string, task: CreateTaskInput): Promise<ClickUpTask> {
    return this.request<ClickUpTask>(`/list/${listId}/task`, {
      method: 'POST',
      body: JSON.stringify(task),
    });
  }

  async getTask(taskId: string): Promise<ClickUpTask> {
    return this.request<ClickUpTask>(`/task/${taskId}`);
  }

  async updateTask(taskId: string, updates: Partial<CreateTaskInput>): Promise<ClickUpTask> {
    return this.request<ClickUpTask>(`/task/${taskId}`, {
      method: 'PUT',
      body: JSON.stringify(updates),
    });
  }

  isRateLimited(): boolean {
    return this.rateLimitRemaining < 5 && Date.now() < this.rateLimitReset;
  }
}

TypeScript Types

// src/clickup/types.ts
interface ClickUpUser {
  id: number;
  username: string;
  email: string;
  color: string;
  profilePicture: string | null;
}

interface ClickUpTeam {
  id: string;
  name: string;
  color: string;
  members: Array<{ user: ClickUpUser; role: number }>;
}

interface ClickUpSpace {
  id: string;
  name: string;
  private: boolean;
  statuses: Array<{ status: string; color: string; type: string }>;
  features: Record<string, { enabled: boolean }>;
}

interface ClickUpTask {
  id: string;
  custom_id: string | null;
  name: string;
  description: string;
  status: { status: string; color: string; type: string };
  priority: { id: string; priority: string; color: string } | null;
  date_created: string;
  date_updated: string;
  due_date: string | null;
  assignees: ClickUpUser[];
  tags: Array<{ name: string }>;
  url: string;
  list: { id: string; name: string };
  folder: { id: string; name: string };
  space: { id: string };
  custom_fields: ClickUpCustomFieldValue[];
}

interface CreateTaskInput {
  name: string;
  description?: string;
  markdown_description?: string;
  assignees?: number[];
  priority?: 1 | 2 | 3 | 4 | null;
  status?: string;
  due_date?: number;
  due_date_time?: boolean;
  parent?: string;
  tags?: string[];
  custom_fields?: Array<{ id: string; value: any }>;
}

interface ClickUpCustomFieldValue {
  id: string;
  name: string;
  type: string;
  value: any;
}

class ClickUpApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly err: string,
    public readonly ecode?: string,
  ) {
    super(`ClickUp API ${status}: ${err}${ecode ? ` (${ecode})` : ''}`);
  }

  get isRateLimited(): boolean { return this.status === 429; }
  get isAuthError(): boolean { return this.status === 401; }
  get isNotFound(): boolean { return this.status === 404; }
  get isRetryable(): boolean { return this.status === 429 || this.status >= 500; }
}

Singleton Pattern

// src/clickup/index.ts
let defaultClient: ClickUpClient | null = null;

export function getClickUpClient(): ClickUpClient {
  if (!defaultClient) {
    const token = process.env.CLICKUP_API_TOKEN;
    if (!token) throw new Error('CLICKUP_API_TOKEN not set');
    defaultClient = new ClickUpClient({ token });
  }
  return defaultClient;
}

Multi-Tenant Factory

const tenantClients = new Map<string, ClickUpClient>();

function getClientForTenant(tenantId: string, token: string): ClickUpClient {
  if (!tenantClients.has(tenantId)) {
    tenantClients.set(tenantId, new ClickUpClient({ token }));
  }
  return tenantClients.get(tenantId)!;
}

Zod Response Validation

import { z } from 'zod';

const TaskSchema = z.object({
  id: z.string(),
  name: z.string(),
  status: z.object({ status: z.string(), color: z.string() }),
  priority: z.object({ priority: z.string() }).nullable(),
  url: z.string().url(),
});

async function getValidatedTask(taskId: string) {
  const raw = await getClickUpClient().getTask(taskId);
  return TaskSchema.parse(raw);
}

Error Handling

PatternUse CaseBenefit
Typed error classAll API callsType-safe error discrimination
SingletonSingle-tenant appsShared rate limit tracking
FactoryMulti-tenant SaaSPer-tenant isolation
Zod validationResponse parsingCatches API contract changes

Resources

Next Steps

Apply patterns in clickup-core-workflow-a for task management.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.