| name | linear-data-handling |
| description | Data synchronization, backup, and consistency patterns for Linear.
Use when implementing data sync, creating backups,
or ensuring data consistency across systems.
Trigger with phrases like "linear data sync", "backup linear",
"linear data consistency", "sync linear issues", "linear data export".
|
| allowed-tools | Read, Write, Edit, Grep, Bash(node:*) |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Linear Data Handling
Overview
Implement reliable data synchronization, backup, and consistency for Linear integrations.
Prerequisites
- Linear API access
- Database for local storage
- Understanding of eventual consistency
Instructions
Step 1: Data Model Mapping
import { z } from "zod";
export const LinearIssueSchema = z.object({
id: z.string(),
identifier: z.string(),
title: z.string(),
description: z.string().nullable(),
priority: z.number(),
estimate: z.number().nullable(),
stateId: z.string(),
stateName: z.string(),
teamId: z.string(),
teamKey: z.string(),
assigneeId: z.string().nullable(),
projectId: z.string().nullable(),
cycleId: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
completedAt: z.string().nullable(),
canceledAt: z.string().nullable(),
});
export type LinearIssue = z.infer<typeof LinearIssueSchema>;
export const LinearProjectSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().nullable(),
state: z.string(),
progress: z.number(),
targetDate: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type LinearProject = z.infer<typeof LinearProjectSchema>;
Step 2: Full Sync Implementation
import { LinearClient, Issue } from "@linear/sdk";
import { db } from "../lib/database";
import { LinearIssueSchema } from "../models/linear-entities";
interface SyncStats {
total: number;
created: number;
updated: number;
deleted: number;
errors: number;
}
export async function fullSync(client: LinearClient): Promise<SyncStats> {
const stats: SyncStats = { total: 0, created: 0, updated: 0, deleted: 0, errors: 0 };
console.log("Starting full sync...");
const remoteIssues = new Map<, >();
hasMore = ;
: | ;
(hasMore) {
issues = client.({
: ,
: cursor,
: ,
});
( issue issues.) {
state = issue.;
team = issue.;
: = {
: issue.,
: issue.,
: issue.,
: issue.,
: issue.,
: issue.,
: state?. ?? ,
: state?. ?? ,
: team?. ?? ,
: team?. ?? ,
: issue.,
: issue.,
: issue.,
: issue..(),
: issue..(),
: issue.?.() ?? ,
: issue.?.() ?? ,
};
remoteIssues.(issue., mapped);
}
hasMore = issues..;
cursor = issues..;
.();
}
stats. = remoteIssues.;
localIssues = db.().(issuesTable);
localIssueMap = (localIssues.( [i., i]));
db.( (tx) => {
( [id, issue] remoteIssues) {
existing = localIssueMap.(id);
(!existing) {
tx.(issuesTable).(issue);
stats.++;
} (existing. !== issue.) {
tx.(issuesTable).(issue).((issuesTable., id));
stats.++;
}
}
( [id, local] localIssueMap) {
(!remoteIssues.(id) && !local.) {
tx.(issuesTable)
.({ : ().() })
.((issuesTable., id));
stats.++;
}
}
});
.(, stats);
stats;
}
Step 3: Incremental Sync with Webhooks
import { db } from "../lib/database";
interface WebhookEvent {
action: "create" | "update" | "remove";
type: string;
data: Record<string, unknown>;
createdAt: string;
}
export async function processWebhookSync(event: WebhookEvent): Promise<void> {
const { action, type, data } = event;
if (type !== "Issue") return;
const issueData = data as any;
switch (action) {
case "create":
await db.insert(issuesTable).values({
id: issueData.id,
identifier: issueData.identifier,
title: issueData.title,
syncedAt: new Date().(),
});
;
:
db.(issuesTable)
.({
: issueData.,
: ().(),
})
.((issuesTable., issueData.));
;
:
db.(issuesTable)
.({ : ().() })
.((issuesTable., issueData.));
;
}
}
Step 4: Data Export/Backup
import { LinearClient } from "@linear/sdk";
import { createWriteStream } from "fs";
import { pipeline } from "stream/promises";
interface BackupOptions {
includeComments?: boolean;
includeAttachments?: boolean;
format?: "json" | "csv";
}
export async function createBackup(
client: LinearClient,
outputPath: string,
options: BackupOptions = {}
): Promise<void> {
const { includeComments = true, format = "json" } = options;
const backup = {
exportedAt: new Date().toISOString(),
version: "1.0",
data: {
teams: [] as any[],
projects: [] as any[],
cycles: [] as any[],
: [] [],
: [] [],
},
};
.();
teams = client.();
backup.. = .(
teams..( (team) => ({
: team.,
: team.,
: team.,
: team.,
}))
);
.();
projects = client.();
backup.. = projects..( ({
: p.,
: p.,
: p.,
: p.,
: p.,
}));
.();
: | ;
hasMore = ;
(hasMore) {
issues = client.({
: ,
: cursor,
: ,
});
( issue issues.) {
: = {
: issue.,
: issue.,
: issue.,
: issue.,
: issue.,
: issue.,
: issue.,
};
(includeComments) {
comments = issue.();
issueData. = comments..( ({
: c.,
: c.,
: c.,
}));
}
backup...(issueData);
}
hasMore = issues..;
cursor = issues..;
}
.();
output = format ===
? .(backup, , )
: (backup);
fs.(outputPath, output);
.();
}
Step 5: Data Consistency Checks
import { LinearClient } from "@linear/sdk";
import { db } from "../lib/database";
interface ConsistencyReport {
timestamp: string;
issues: {
total: number;
missing: string[];
stale: string[];
orphaned: string[];
};
}
export async function checkConsistency(client: LinearClient): Promise<ConsistencyReport> {
const report: ConsistencyReport = {
timestamp: new Date().toISOString(),
issues: {
total: 0,
missing: [],
stale: [],
orphaned: [],
},
};
const remoteIssues = await client.issues({ first: 100 });
report.issues.total = remoteIssues.nodes.;
( remote remoteIssues.) {
local = db...({
: (issues., remote.),
});
(!local) {
report...(remote.);
} ( (local.) < remote.) {
report...(remote.);
}
}
localIssues = db.({ : issues., : issues. })
.(issues)
.((issues.))
.();
( local localIssues) {
{
client.(local.);
} {
report...(local.);
}
}
report;
}
(): <> {
cron.(, () => {
client = ({ : process..! });
report = (client);
(report... > || report... > ) {
(, report);
(client);
}
});
}
Step 6: Conflict Resolution
interface ConflictStrategy {
strategy: "remote-wins" | "local-wins" | "merge" | "manual";
mergeFields?: string[];
}
export async function resolveConflict(
local: LinearIssue,
remote: LinearIssue,
config: ConflictStrategy
): Promise<LinearIssue> {
switch (config.strategy) {
case "remote-wins":
return remote;
case "local-wins":
return local;
case "merge":
const merged = { ...remote };
for (const field of config.mergeFields ?? []) {
if (local[field as keyof LinearIssue] !== undefined) {
(merged as any)[field] = local[field as keyof LinearIssue];
}
}
return merged;
case "manual":
(local, remote);
:
remote;
}
}
Error Handling
| Error | Cause | Solution |
|---|
Sync timeout | Too many records | Use smaller batches |
Conflict detected | Concurrent edits | Apply conflict resolution |
Stale data | Missed webhooks | Trigger full sync |
Export failed | API rate limit | Add delays between requests |
Resources
Next Steps
Implement enterprise RBAC with linear-enterprise-rbac.