linear-data-handling
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".
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
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.