К основному содержанию
скилл CX Codex

sync-guide — скилл для Claude Code: что делает и как установить

Скилл учит ассистента проектировать интеграции с Notion Workers: настраивать пакетную загрузку, разделять backfill и delta-потоки и удалять устаревшие записи.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add makenotion/notion-cookbook --skill sync-guide -a claude-code
Открыть на GitHub

Что делает sync-guide

Скилл подключает руководство по разработке синхронизаций для @notionhq/workers. Модель учится выбирать между полной перезаписью базы и связкой из двух процессов: ручного бэкфилла для полной выгрузки и инкрементального дельта-синка по расписанию. Такой подход защищает от сбоев в конечных автоматах и не даёт устаревшим ответам затирать свежие правки при передаче поля upstreamUpdatedAt.

Внутри собраны шаблоны пагинации по курсорам или ключам, буфер задержки отметки времени для систем с отложенной согласованностью и настройка лимитов через worker.pacer. Для очистки удалённых строк скилл предлагает либо маркеры удаления, либо автоматическую очистку в режиме replace.

Инструкция загружается автоматически при обнаружении задач на синхронизацию, потому что флаг user-invocable установлен в false. Без установленного пакета @notionhq/workers запустить сгенерированный код не выйдет.

Какие задачи решает

  • настроить инкрементальный сбор изменений через дельта-синк
  • связать первичный бэкфилл и частые дельта-обновления одной базы Notion
  • настроить буфер задержки курсора для источников с отложенной согласованностью
  • расставить лимиты запросов к внешнему API через worker.pacer

Примеры запросов агенту

  • Напиши воркер для выгрузки контактов из внешней CRM в базу Notion с поддержкой инкрементальных правок
  • Спроектируй два синка для одной базы Notion: ручной бэкфилл и регулярную дельту
  • Добавь буфер согласованности по времени для курсора обновления записей

Как установить sync-guide

1Через Skills CLI (Vercel)

npx skills add makenotion/notion-cookbook --skill sync-guide -a claude-code

2Вручную: положить файлы

~/.claude/skills/sync-guide/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/sync-guide/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о sync-guide

Как установить sync-guide в Claude Code?

Выполните команду npx skills add makenotion/notion-cookbook --skill sync-guide -a claude-code.

Что делает скилл sync-guide?

Он даёт ассистенту правила архитектуры синхронизаций Notion Workers, объясняя выбор между полной заменой данных и связкой бэкфилла с инкрементальным потоком.

Можно ли вызвать этот скилл вручную через слэш-команду?

Нет, параметр user-invocable равен false, поэтому агент подтягивает руководство самостоятельно, когда видит в проекте код синхронизации.

Исходный текст

SKILL.md · MIT
---
name: sync-guide
description: Comprehensive guide to building Notion Workers syncs — covers the two-sync architecture (backfill+delta), replace mode, pagination, consistency buffers, pacers, deletion strategies, and common pitfalls. Auto-loads when sync-related work is detected.
user-invocable: false
---

## What is a Sync?

A sync is a recurring `execute` function that returns data changes to populate a Notion database. The runtime calls `execute` in a loop:

```ts
const db = worker.database("myDb", {
  type: "managed",
  initialTitle: "My Data",
  primaryKeyProperty: "ID",
  schema: {
    properties: {
      Name: Schema.title(),
      ID: Schema.richText(),
    },
  },
})

worker.sync("mySync", {
  database: db,
  execute: async (state, { notion }) => ({
    changes: [
      {
        type: "upsert",
        key: "1",
        properties: {
          Name: Builder.title("Item 1"),
          ID: Builder.richText("1"),
        },
      },
    ],
    hasMore: false,
    nextState: undefined,
  }),
})
```

Each call returns `{ changes, hasMore, nextState }`. If `hasMore` is `true`, the runtime calls `execute` again with `nextState`. This continues until `hasMore` is `false`, completing a **cycle**. The next cycle begins at the scheduled interval with the state from the end of the previous cycle.

**Imports:**

```ts
import { Worker } from "@notionhq/workers"
import * as Builder from "@notionhq/workers/builder"
import * as Schema from "@notionhq/workers/schema"
```

## Decision Framework

### Step 1: Choose an Architecture

The deciding factor is **API capability and dataset size**. Two tiers:

| Condition                                                         | Architecture                                                       |
| ----------------------------------------------------------------- | ------------------------------------------------------------------ |
| Small source (<1k records) or API with no change tracking         | **Simple replace sync** — one sync, `mode: "replace"`              |
| Everything else (API supports `updated_at`, change feeds, events) | **Backfill + delta pair** — two syncs writing to the same database |

**Simple replace sync**: One sync returns the full dataset each cycle. After the final `hasMore: false`, any records not seen are deleted automatically. Use when the dataset is small enough to re-fetch entirely.

**Backfill + delta pair**: Two syncs share a single database. The **backfill sync** (`mode: "replace"`, `schedule: "manual"`) re-fetches everything when triggered. The **delta sync** (`mode: "incremental"`, frequent schedule) fetches only changes since the last run. This separates concerns cleanly — no bi-modal state machine, no backfill-to-delta transition bugs.

Initialize the delta cursor before starting the backfill. When both syncs can
write the same record, include `upstreamUpdatedAt` on each upsert so a stale
backfill response cannot overwrite a newer delta response.

### Step 2: Understand Your API's Pagination

Most APIs require paginating through results. Return batches of ~100 changes. Returning too many changes in one `execute` call will fail.

**Backfill pagination** (full dataset load):

1. **Opaque cursor token** — GraphQL `endCursor`, Stripe `starting_after`
2. **Page number / offset** — `?page=N&limit=100`
3. **Keyset (timestamp + id)** — `WHERE created_at > X OR (created_at = X AND id > Y)` — the gold standard for timestamp-sorted mutable data

**Delta pagination** (change-only loads, incremental mode):

1. **Timestamp cursor** — `?updated_since=<cursor>` with consistency buffer
2. **Keyset on updated_at + id** — same keyset pattern on the modification timestamp
3. **Event/changelog feed** — `GET /events?after=<eventId>`
4. **Same opaque cursor** — when the API sorts by `updated_at`, the backfill cursor works for delta too

### Step 3: Consistency Buffer (Delta Syncs)

APIs tend to be eventually consistent. A record that was just written or updated may not appear in query results immediately. Since the cursor never resets in incremental mode, if it advances past a record that hasn't been indexed yet, that record is skipped permanently. Lag the cursor 10-60 seconds behind "now":

```ts
const bufferMs = 15_000
const maxCursor = new Date(Date.now() - bufferMs).toISOString()
function minTimestamp(a: string, b: string): string {
  return Date.parse(a) <= Date.parse(b) ? a : b
}

const nextCursor =
  records.length > 0 ? minTimestamp(lastRecord.updatedAt, maxCursor) : maxCursor
```

### Step 4: Deletion Strategies

1. **Backfill sync (replace mode)**: free — unseen records are auto-deleted each cycle. This is the primary mechanism for handling deletes when the API has no delete signal.
2. **Delta sync with delete API**: emit `{ type: "delete", key }` markers. If the delete signal comes from a separate endpoint (audit log, archived filter), use the **flip-flop pattern**: run the main delta stream until caught up (`hasMore: false`), then switch to the delete stream for a cycle, then back. Both cursors persist in state independently.
3. **No delete API, large dataset**: rely on the backfill sync's replace-mode mark-and-sweep. Trigger the backfill manually or on a slow schedule to clean up stale records.

## Replace Mode

Simple: fetch everything, return it all, let the runtime handle deletes. Use as a standalone sync for small sources, or as the backfill half of a backfill+delta pair.

```ts
const db = worker.database("records", {
  type: "managed",
  initialTitle: "Records",
  primaryKeyProperty: "ID",
  schema: {
    properties: { Name: Schema.title(), ID: Schema.richText() },
  },
})

const apiPacer = worker.pacer("myApi", {
  allowedRequests: 10,
  intervalMs: 1000,
})

worker.sync("recordsBackfill", {
  database: db,
  mode: "replace",
  schedule: "manual", // trigger manually or on a slow schedule
  execute: async (state) => {
    const page = state?.page ?? 1
    await apiPacer.wait()
    const { items, totalPages } = await fetchPage(page, 100)
    const hasMore = page < totalPages
    return {
      changes: items.map((item) => ({
        type: "upsert" as const,
        key: item.id,
        properties: {
          Name: Builder.title(item.name),
          ID: Builder.richText(item.id),
        },
      })),
      hasMore,
      nextState: hasMore ? { page: page + 1 } : undefined,
    }
  },
})
```

See `examples/replace-simple.ts` and `examples/replace-paginated.ts` for complete working examples.

## Incremental Mode (Delta Sync)

The delta sync fetches only changes since the last run. When paired with a replace-mode backfill sync on the same database, this replaces the old bi-modal single-sync pattern.

```ts
// Reuses the same `db` and `apiPacer` from above

worker.sync("recordsDelta", {
  database: db,
  mode: "incremental",
  schedule: "5m",
  execute: async (state: { cursor: string } | undefined) => {
    const cursor = state?.cursor ?? new Date(0).toISOString()
    const bufferTs = new Date(Date.now() - 15_000).toISOString()

    await apiPacer.wait()
    // fetchChanges must apply bufferTs as an upstream upper bound.
    const { items, nextCursor } = await fetchChanges(cursor, bufferTs)
    const done = !nextCursor

    return {
      changes: items.map(toUpsert),
      hasMore: !done,
      nextState: {
        // This example assumes nextCursor is an ISO timestamp. Opaque cursors
        // must not be compared with the timestamp buffer.
        cursor: done
          ? minTimestamp(nextCursor ?? cursor, bufferTs)
          : nextCursor,
      },
    }
  },
})
```

**Key points:**

- The delta sync's state is simple — just a cursor. No phase discrimination needed.
- The backfill sync (replace mode) handles the initial full load and periodic cleanup of deleted records.
- Both syncs write to the same database via the shared `db` handle.
- The pacer is shared between syncs — the server apportions the budget evenly.

See `examples/incremental-basic.ts`, `examples/incremental-bimodal.ts`, and `examples/incremental-events.ts` for complete patterns.

## Schema Reference

Define the Notion database shape with `Schema` types and build values with `Builder`:

| Schema type                      | Builder value                              | Notes                                                                                                                                                                                     |
| -------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Schema.title()`                 | `Builder.title("text")`                    | Primary display field. Every schema needs exactly one.                                                                                                                                    |
| `Schema.richText()`              | `Builder.richText("text")`                 | Text content, IDs                                                                                                                                                                         |
| `Schema.url()`                   | `Builder.url("https://...")`               | URL field                                                                                                                                                                                 |
| `Schema.email()`                 | `Builder.email("[email protected]")`                 | Email field                                                                                                                                                                               |
| `Schema.phoneNumber()`           | `Builder.phoneNumber("+1...")`             | Phone field                                                                                                                                                                               |
| `Schema.checkbox()`              | `Builder.checkbox(true)`                   | Boolean                                                                                                                                                                                   |
| `Schema.file()`                  | `Builder.file("https://...", "name")`      | File URL + optional display name                                                                                                                                                          |
| `Schema.number()`                | `Builder.number(42)`                       | Number. Optional format: `Schema.number("percent")`                                                                                                                                       |
| `Schema.date()`                  | `Builder.date("2024-01-15")`               | Date (YYYY-MM-DD). Also: `Builder.dateTime("2024-01-15T10:30:00Z")`, `Builder.dateRange(start, end)`                                                                                      |
| `Schema.select([...])`           | `Builder.select("Option A")`               | Single select. Define options: `Schema.select([{ name: "A" }, { name: "B" }])`. **Options must have non-empty `name` values** — `Schema.select([])` and `{ name: "" }` are not supported. |
| `Schema.multiSelect([...])`      | `Builder.multiSelect("A", "B")`            | Multi select                                                                                                                                                                              |
| `Schema.status(...)`             | `Builder.status("Done")`                   | Status with groups                                                                                                                                                                        |
| `Schema.people()`                | `Builder.people("[email protected]")`           | People by email                                                                                                                                                                           |
| `Schema.place()`                 | `Builder.place({ lat: 40.7, lon: -74.0 })` | Geographic location                                                                                                                                                                       |
| `Schema.relation("databaseKey")` | `[Builder.relation("pk")]`                 | Relation to another managed database. Value is an **array**.                                                                                                                              |

Relations use the related database key. Two-way relations are configured the same way:

```ts
Schema.relation("otherDatabase", {
  twoWay: true,
  relatedPropertyName: "Back Link",
})
```

Row-level icons and page content:

```ts
changes: [
  {
    type: "upsert",
    key: "1",
    properties: {
      Name: Builder.title("Example"),
      // ... other properties ...
    },
    icon: Builder.emojiIcon("🎯"), // or Builder.notionIcon("rocket", "blue")
    pageContentMarkdown: "## Details\nSome text", // Markdown body for the page
  },
]
```

## Common Mistakes

1. **Not using a pacer** — every API call inside `execute` should be preceded by `await apiPacer.wait()`. Without it, syncs will hit rate limits and fail.
2. **Missing consistency buffer on delta syncs** — the cursor will permanently skip records not yet indexed in eventually consistent APIs.
3. **Not paginating** — returning too many changes at once. Start with batches of ~100.
4. **Using replace mode for large datasets** — if the API supports change tracking, pair a replace-mode backfill sync with an incremental delta sync instead of re-fetching everything each cycle.
5. **Cursor that doesn't advance** — infinite loop. Ensure `nextState` changes between iterations.
6. **Forgetting first-run handling** — `state` is `undefined` on first call. Use `state?.cursor ?? null`.
7. **Forgetting that backfill + delta share a database** — both syncs must use the same `worker.database()` handle and the same key/properties shape.
8. **Not triggering the backfill sync** — the backfill sync with `schedule: "manual"` won't run automatically. Trigger it on deploy or periodically to clean up deleted records.
9. **Empty select values** — `Schema.select()` requires at least one option with a non-empty `name`. `Schema.select([])` and `{ name: "" }` are not supported.

## CLI Commands for Sync Development

```shell
# Deploy
ntn workers deploy

# Preview (test without writing)
ntn workers sync trigger <key> --preview
ntn workers sync trigger <key> --preview --context '<json>'  # continue pagination

# Trigger a sync run
ntn workers sync trigger <key>

# Check sync status
ntn workers sync status

# View run logs
ntn workers runs list
ntn workers runs list --plain | head -n1 | cut -f1 | xargs -I{} ntn workers runs logs {}

# Reset state (full re-backfill)
ntn workers sync state reset <key>

# Manage secrets
ntn workers env set KEY=value
ntn workers env push
```

## API Patterns Reference

See [api-pagination-patterns.md](./api-pagination-patterns.md) for detailed strategies drawn from production syncs with Salesforce, Stripe, HubSpot, GitHub, and ServiceNow.

© makenotion · MIT · makenotion/notion-cookbook

Похожие скиллы

Все скиллы по теме «Бэкенд и API» →
mcp-builder официальный anthropics Скилл проводит через создание MCP-сервера на TypeScript или Python: от изучения протокола до тестов и оценки качества. 130 949установок claude-api официальный anthropics Справочник по Anthropic SDK и API Claude для генерации кода без устаревших параметров и выдуманных методов. 74 198установок lark-im larksuite Скилл подключает Lark IM к агенту: он отправляет сообщения, ищет историю, управляет чатами и файлами. Подойдёт для задач вокруг командных коммуникаций. CX Codex +1 472 900установок lark-event larksuite Слушает события Lark/Feishu и отдаёт их потоком NDJSON. Подходит для ботов, обработчиков сообщений и долгих подписчиков. CX Codex +1 466 800установок caveman-discover JuliusBrussee Находит точки вызова LLM в репозитории и размечает их воркфлоу для Caveman Cloud, чтобы расходы на токены не сливались в одну общую кучу. CX Codex +1 104 400установок migration JuliusBrussee Скилл задаёт правила безопасных миграций схем, данных и API с обязательным откатом. Пригодится при изменениях, где нельзя терять данные или ломать совместимость. CX Codex +1 103 900установок

Из того же набора20

workflow-validate Разбирает файлы в src/workflows/ и находит ошибки типизации триггеров и небезопасные повторы шагов. Пригодится перед выкаткой воркфлоу в прод. CX Codex +1 ★ 220звёзд репо workflow-guide Руководство для агента по написанию надёжных воркфлоу в Notion Cookbook с изолированными шагами, защитой от повторов и безопасным реплеем данных. CX Codex +1 ★ 220звёзд репо sync Скилл помогает спроектировать и добавить синхронизацию данных в Notion Worker. Подходит, когда нужно выбрать режим, схему и пагинацию. CX Codex +1 ★ 220звёзд репо sync-validate Скилл проходит по коду синхронизации и ищет типичные баги: курсор не двигается, пагинация не завершается, теряются удаления. CX Codex +1 ★ 220звёзд репо sync-debug Скилл разбирает сбои синхронизации по логам и коду, а затем предлагает исправление и проверку. Пригодится, когда запуск завершился ошибкой или вернул неверные данные. CX Codex +1 ★ 220звёзд репо custom-blocks Помогает собрать интерактивный интерфейс внутри Notion, который читает и меняет данные рабочего пространства. CX Codex +1 ★ 220звёзд репо auth-guide Настраивает авторизацию во внешних сервисах для Notion Workers: токены, проксированные учётные данные и OAuth. Пригодится при интеграции воркера со сторонними API вроде GitHub или Stripe. CX Codex +1 ★ 220звёзд репо workflow Помогает создавать и проверять workflow для Notion App с типизированными триггерами и безопасными при повторном запуске шагами. Перед правками он требует прочитать инструкцию SDK. CX Codex +1 ★ 220звёзд репо

Спонсоры проекта

Компании, которые помогают держать аналитику открытой