Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Handle Figma API data correctly: comments, versions, user data, and privacy compliance. Use when working with Figma comments API, version history, or ensuring GDPR compliance for Figma user data. Trigger with phrases like "figma data", "figma comments", "figma versions", "figma GDPR", "figma user data".
.claude/skills/jeremylongshore-figma-data-handling/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -18% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 74% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 83% | 0% |
Work with Figma's data APIs: comments, version history, and user information. Handle sensitive data correctly with redaction and privacy compliance.
FIGMA_PAT with appropriate scopes (file_comments:read/write, file_versions:read)typescriptconst PAT = process.env.FIGMA_PAT!; const FILE_KEY = process.env.FIGMA_FILE_KEY!; // GET /v1/files/:key/comments -- requires file_comments:read scope async function getComments(fileKey: string) { const res = await fetch( `https://api.figma.com/v1/files/${fileKey}/comments`, { headers: { 'X-Figma-Token': PAT } } ); const data = await res.json(); // data.comments is an array of: // { id, message, file_key, parent_id, user, client_meta, resolved_at, created_at, order_id } return data.comments; } // GET with as_md=true to get rich-text comments as markdown async function getCommentsAsMarkdown(fileKey: string) { const res = await fetch( `https://api.figma.com/v1/files/${fileKey}/comments?as_md=true`, { headers: { 'X-Figma-Token': PAT } } ); return (await res.json()).comments; } // POST /v1/files/:key/comments -- requires file_comments:write scope async function postComment(fileKey: string, message: string, nodeId?: string) { const body: any = { message }; if (nodeId) { body.client_meta = { node_id: nodeId }; } const res = await fetch( `https://api.figma.com/v1/files/${fileKey}/comments`, { method: 'POST', headers: { 'X-Figma-Token': PAT, 'Content-Type': 'application/json', }, body: JSON.stringify(body), } ); return res.json(); } // POST reactions to a comment -- requires file_comments:write async function reactToComment(fileKey: string, commentId: string, emoji: string) { return fetch( `https://api.figma.com/v1/files/${fileKey}/comments/${commentId}/reactions`, { method: 'POST', headers: { 'X-Figma-Token': PAT, 'Content-Type': 'application/json', }, body: JSON.stringify({ emoji }), } ).then(r => r.json()); }
typescript// GET /v1/files/:key/versions -- requires file_versions:read scope async function getVersionHistory(fileKey: string) { const res = await fetch( `https://api.figma.com/v1/files/${fileKey}/versions`, { headers: { 'X-Figma-Token': PAT } } ); const data = await res.json(); // data.versions: array of { id, created_at, label, description, user } // Ordered by created_at (most recent first) return data.versions; } // Paginate through all versions async function getAllVersions(fileKey: string) { const versions: any[] = []; let url: string | null = `https://api.figma.com/v1/files/${fileKey}/versions`; while (url) { const res = await fetch(url, { headers: { 'X-Figma-Token': PAT } }); const data = await res.json(); versions.push(...data.versions); // Pagination uses cursor-based pagination url = data.pagination?.next_page ? `https://api.figma.com/v1/files/${fileKey}/versions?before=${data.pagination.next_page}` : null; } return versions; }
typescript// GET /v1/me -- returns authenticated user interface FigmaUser { id: string; handle: string; img_url: string; email: string; // PII -- handle carefully } // Redact PII before logging or storing function redactFigmaUser(user: FigmaUser): Omit<FigmaUser, 'email'> & { email: string } { return { ...user, email: '[REDACTED]', img_url: '[REDACTED]', }; } // Data classification for Figma responses interface DataClassification { field: string; sensitivity: 'public' | 'internal' | 'pii'; handling: string; } const figmaDataClassification: DataClassification[] = [ { field: 'user.email', sensitivity: 'pii', handling: 'Encrypt at rest, redact in logs' }, { field: 'user.handle', sensitivity: 'internal', handling: 'Do not expose to unauthorized users' }, { field: 'user.img_url', sensitivity: 'pii', handling: 'Do not cache without consent' }, { field: 'file.name', sensitivity: 'internal', handling: 'Standard handling' }, { field: 'comment.message', sensitivity: 'internal', handling: 'May contain PII -- scan before storing' }, { field: 'PAT token', sensitivity: 'pii', handling: 'Never log, never store in code' }, ];
typescript// Figma image export URLs expire after 30 days // Plan data retention accordingly interface CachedFigmaData { data: any; fetchedAt: Date; expiresAt: Date; } function createCacheEntry(data: any, ttlMs: number): CachedFigmaData { const now = new Date(); return { data, fetchedAt: now, expiresAt: new Date(now.getTime() + ttlMs), }; } // Cleanup expired entries async function cleanupExpiredData(db: any) { const now = new Date(); const deleted = await db.figmaCache.deleteMany({ expiresAt: { $lt: now }, }); console.log(`Cleaned up ${deleted.count} expired Figma cache entries`); }
typescript// Never log these fields from Figma responses const REDACT_FIELDS = ['email', 'img_url', 'access_token', 'refresh_token']; function safeFigmaLog(label: string, data: any) { const safe = JSON.parse(JSON.stringify(data)); function redact(obj: any) { for (const key of Object.keys(obj)) { if (REDACT_FIELDS.includes(key)) { obj[key] = '[REDACTED]'; } else if (typeof obj[key] === 'object' && obj[key] !== null) { redact(obj[key]); } } } redact(safe); console.log(`[figma] ${label}:`, JSON.stringify(safe)); }
| Error | Cause | Solution | |-------|-------|----------| | 403 on comments | Missing file_comments:read scope | Regenerate PAT with scope | | Empty version history | New file with no saved versions | Create a named version in Figma first | | PII in logs | Missing redaction | Apply safeFigmaLog wrapper | | Stale image URLs | URLs older than 30 days | Re-export images; do not cache URLs long-term |
Pull the latest comments as Markdown and post a reaction (Step 1 Comments API):
bashcurl -s -H "X-Figma-Token: ${FIGMA_PAT}" \ "https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/comments?as_md=true" \ | jq -r '.comments[0] | "\(.user.handle): \(.message)"' # mia.designer: Updated the button radius to 8px — ready for review
Walk version history with pagination (Step 2):
bashcurl -s -H "X-Figma-Token: ${FIGMA_PAT}" \ "https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/versions" \ | jq '{versions: [.versions[] | {id, created_at, label}], next: .pagination.next_page}'
PII rules for what you may persist from these payloads (user handles, avatars, emails): references/user-data-and-privacy.md and references/safe-logging.md.
For enterprise access control, see figma-enterprise-rbac.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 26,491 | 16,332 | -38% | 1 | 1 | 0% | 5,721 | 4,697 | -18% | 0 | 0 | — |
case-02 | pass→fail | 21,958 | 14,490 | -34% | 1 | 1 | 0% | 3,471 | 5,088 | +47% | 0 | 0 | — |
case-03 | fail→fail | 27,516 | 24,774 | -10% | 1 | 1 | 0% | 5,608 | 6,096 | +9% | 0 | 0 | — |
case-04 | pass→pass | 12,019 | 11,482 | -4% | 1 | 1 | 0% | 2,171 | 4,261 | +96% | 0 | 0 | — |
case-05 | pass→pass | 15,311 | 16,943 | +11% | 1 | 1 | 0% | 2,412 | 5,123 | +112% | 0 | 0 | — |
case-06 | pass→pass | 18,940 | 27,461 | +45% | 1 | 1 | 0% | 3,286 | 6,313 | +92% | 0 | 0 | — |
case-07 | fail→pass | 13,472 | 5,437 | -60% | 1 | 1 | 0% | 2,321 | 3,354 | +45% | 0 | 0 | — |
case-08 | pass→pass | 7,370 | 4,819 | -35% | 1 | 1 | 0% | 1,467 | 3,144 | +114% | 0 | 0 | — |
case-09 | fail→pass | 7,810 | 4,836 | -38% | 1 | 1 | 0% | 1,394 | 3,139 | +125% | 0 | 0 | — |
case-10 | fail→pass | 9,667 | 3,182 | -67% | 1 | 1 | 0% | 1,687 | 2,935 | +74% | 0 | 0 | — |
case-11 | fail→pass | 8,787 | 3,809 | -57% | 1 | 1 | 0% | 1,554 | 2,849 | +83% | 0 | 0 | — |
case-12 | fail→pass | 11,334 | 4,275 | -62% | 1 | 1 | 0% | 2,130 | 3,142 | +48% | 0 | 0 | — |
case-13 | fail→pass | 8,128 | 4,316 | -47% | 1 | 1 | 0% | 1,426 | 2,870 | +101% | 0 | 0 | — |
case-14 | pass→pass | 13,937 | 9,509 | -32% | 1 | 1 | 0% | 2,470 | 4,040 | +64% | 0 | 0 | — |
case-15 | fail→pass | 10,163 | 7,288 | -28% | 1 | 1 | 0% | 1,913 | 3,456 | +81% | 0 | 0 | — |
case-16 | fail→pass | 11,361 | 922,920 | +8024% | 1 | 1 | 0% | 1,660 | 2,788 | +68% | 0 | 0 | — |
case-17 | pass→pass | 13,095 | 14,846 | +13% | 1 | 1 | 0% | 2,209 | 4,481 | +103% | 0 | 0 | — |
case-18 | fail→pass | 12,682 | 4,893 | -61% | 1 | 1 | 0% | 2,346 | 3,023 | +29% | 0 | 0 | — |
case-19 | pass→pass | 14,414 | 6,732 | -53% | 1 | 1 | 0% | 2,060 | 3,436 | +67% | 0 | 0 | — |
case-20 | pass→pass | 11,069 | 6,477 | -41% | 1 | 1 | 0% | 1,959 | 3,386 | +73% | 0 | 0 | — |
case-21 | pass→pass | 9,482 | 2,103 | -78% | 1 | 1 | 0% | 1,496 | 2,604 | +74% | 0 | 0 | — |
case-22 | fail→pass | 5,638 | 1,780 | -68% | 1 | 1 | 0% | 1,056 | 2,538 | +140% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +45 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.