Claude
Skills
Sign in
Back

linear-data-handling

Included with Lifetime
$97 forever

Data synchronization, backup, and consistency patterns for Linear. Use when implementing data sync, creating backups, exporting data, or ensuring data consistency between Linear and local state. Trigger: "linear data sync", "backup linear", "linear export", "linear data consistency", "sync linear issues".

Generalsaaslinearbackup

What this skill does

# Linear Data Handling

## Overview

Implement reliable data synchronization, backup, and consistency for Linear integrations. Covers full sync, incremental webhook sync, JSON/CSV export, consistency checks, and conflict resolution.

## Prerequisites

- `@linear/sdk` with API key configured
- Database for local storage (any ORM — Drizzle, Prisma, Knex)
- Understanding of eventual consistency

## Instructions

### Step 1: Data Model Schema

```typescript
// src/models/linear-entities.ts
import { z } from "zod";

export const LinearIssueSchema = z.object({
  id: z.string().uuid(),
  identifier: z.string(), // e.g., "ENG-123"
  title: z.string(),
  description: z.string().nullable(),
  priority: z.number().int().min(0).max(4),
  estimate: z.number().nullable(),
  stateId: z.string().uuid(),
  stateName: z.string(),
  stateType: z.string(),
  teamId: z.string().uuid(),
  teamKey: z.string(),
  assigneeId: z.string().uuid().nullable(),
  projectId: z.string().uuid().nullable(),
  cycleId: z.string().uuid().nullable(),
  parentId: z.string().uuid().nullable(),
  dueDate: z.string().nullable(),
  createdAt: z.string(),
  updatedAt: z.string(),
  completedAt: z.string().nullable(),
  canceledAt: z.string().nullable(),
  syncedAt: z.string(),
});

export type LinearIssue = z.infer<typeof LinearIssueSchema>;
```

### Step 2: Full Sync

Paginate through all issues, resolve relations, and upsert locally.

```typescript
import { LinearClient } from "@linear/sdk";

interface SyncStats {
  total: number;
  created: number;
  updated: number;
  deleted: number;
  errors: number;
}

async function fullSync(client: LinearClient, teamKey: string): Promise<SyncStats> {
  const stats: SyncStats = { total: 0, created: 0, updated: 0, deleted: 0, errors: 0 };
  const remoteIds = new Set<string>();

  // Paginate all issues
  let cursor: string | undefined;
  let hasNext = true;

  while (hasNext) {
    const result = await client.client.rawRequest(`
      query FullSync($teamKey: String!, $cursor: String) {
        issues(
          first: 100,
          after: $cursor,
          filter: { team: { key: { eq: $teamKey } } },
          orderBy: updatedAt
        ) {
          nodes {
            id identifier title description priority estimate
            dueDate createdAt updatedAt completedAt canceledAt
            state { id name type }
            team { id key }
            assignee { id }
            project { id }
            cycle { id }
            parent { id }
          }
          pageInfo { hasNextPage endCursor }
        }
      }
    `, { teamKey, cursor });

    const issues = result.data.issues;

    for (const issue of issues.nodes) {
      remoteIds.add(issue.id);
      stats.total++;

      try {
        const mapped: LinearIssue = {
          id: issue.id,
          identifier: issue.identifier,
          title: issue.title,
          description: issue.description,
          priority: issue.priority,
          estimate: issue.estimate,
          stateId: issue.state.id,
          stateName: issue.state.name,
          stateType: issue.state.type,
          teamId: issue.team.id,
          teamKey: issue.team.key,
          assigneeId: issue.assignee?.id ?? null,
          projectId: issue.project?.id ?? null,
          cycleId: issue.cycle?.id ?? null,
          parentId: issue.parent?.id ?? null,
          dueDate: issue.dueDate,
          createdAt: issue.createdAt,
          updatedAt: issue.updatedAt,
          completedAt: issue.completedAt,
          canceledAt: issue.canceledAt,
          syncedAt: new Date().toISOString(),
        };

        const existing = await db.issues.findById(issue.id);
        if (existing) {
          await db.issues.update(issue.id, mapped);
          stats.updated++;
        } else {
          await db.issues.insert(mapped);
          stats.created++;
        }
      } catch (error) {
        stats.errors++;
        console.error(`Error syncing ${issue.identifier}:`, error);
      }
    }

    hasNext = issues.pageInfo.hasNextPage;
    cursor = issues.pageInfo.endCursor;

    // Rate limit protection
    if (hasNext) await new Promise(r => setTimeout(r, 100));
  }

  // Soft-delete issues that no longer exist remotely
  const localIds = await db.issues.listIds({ teamKey });
  for (const localId of localIds) {
    if (!remoteIds.has(localId)) {
      await db.issues.softDelete(localId);
      stats.deleted++;
    }
  }

  console.log(`Full sync complete:`, stats);
  return stats;
}
```

### Step 3: Incremental Sync via Webhooks

```typescript
async function processWebhookSync(event: {
  action: "create" | "update" | "remove";
  type: string;
  data: any;
}) {
  if (event.type !== "Issue") return;

  const syncedAt = new Date().toISOString();

  switch (event.action) {
    case "create":
      await db.issues.insert({
        id: event.data.id,
        identifier: event.data.identifier,
        title: event.data.title,
        description: event.data.description,
        priority: event.data.priority,
        estimate: event.data.estimate,
        stateId: event.data.stateId ?? event.data.state?.id,
        stateName: event.data.state?.name ?? "Unknown",
        stateType: event.data.state?.type ?? "unknown",
        teamId: event.data.teamId ?? event.data.team?.id,
        teamKey: event.data.team?.key ?? "",
        assigneeId: event.data.assigneeId ?? null,
        projectId: event.data.projectId ?? null,
        cycleId: event.data.cycleId ?? null,
        parentId: event.data.parentId ?? null,
        dueDate: event.data.dueDate ?? null,
        createdAt: event.data.createdAt,
        updatedAt: event.data.updatedAt,
        completedAt: event.data.completedAt ?? null,
        canceledAt: event.data.canceledAt ?? null,
        syncedAt,
      });
      break;

    case "update":
      await db.issues.update(event.data.id, {
        ...event.data,
        syncedAt,
      });
      break;

    case "remove":
      await db.issues.softDelete(event.data.id);
      break;
  }
}
```

### Step 4: Data Export / Backup

```typescript
async function exportToJson(client: LinearClient, outputDir: string) {
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const teams = await client.teams();

  const backup = {
    exportedAt: new Date().toISOString(),
    version: "1.0",
    teams: teams.nodes.map(t => ({ id: t.id, key: t.key, name: t.name })),
    projects: [] as any[],
    issues: [] as any[],
  };

  // Export projects
  const projects = await client.projects();
  backup.projects = projects.nodes.map(p => ({
    id: p.id, name: p.name, state: p.state,
    targetDate: p.targetDate, progress: p.progress,
  }));

  // Export issues with pagination
  for (const team of teams.nodes) {
    let cursor: string | undefined;
    let hasNext = true;
    while (hasNext) {
      const result = await client.issues({
        first: 100,
        after: cursor,
        filter: { team: { id: { eq: team.id } } },
      });
      for (const issue of result.nodes) {
        backup.issues.push({
          id: issue.id,
          identifier: issue.identifier,
          title: issue.title,
          description: issue.description,
          priority: issue.priority,
          estimate: issue.estimate,
          createdAt: issue.createdAt,
          updatedAt: issue.updatedAt,
        });
      }
      hasNext = result.pageInfo.hasNextPage;
      cursor = result.pageInfo.endCursor;
      if (hasNext) await new Promise(r => setTimeout(r, 100));
    }
  }

  const path = `${outputDir}/linear-backup-${timestamp}.json`;
  await fs.writeFile(path, JSON.stringify(backup, null, 2));
  console.log(`Exported ${backup.issues.length} issues to ${path}`);
}
```

### Step 5: Consistency Check

```typescript
async function checkConsistency(client: LinearClient, teamKey: string): Promise<{
  missing: string[];
  stale: string[];
  orphaned: string[];
}> {
  // Sample 50 remote issues
  const remote = await client.issues({
    first: 50,
  

Related in General