はじめに
hkob の雑記録の第494回目(連続67日目)は、昨日に引き続きテンプレートの中身を確認していきます。次は skills の中にある sync の SKILL.md を読んでみます。
sync/SKILL.md
name: sync description: ガイド付きセットアップで新しい sync 機能をひな形生成する。データソース、モード、ページネーション、カーソル設計について質問し、最後に動作するコードを生成する。 user-invocable: true disable-model-invocation: true allowed-tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep", "Agent"]
最初に SKILL の概要が書かれています。わからない部分は Notion AI に説明してもらいました。
user-invocable: trueは「このスキルはユーザーが明示的に呼び出したときだけ実行できる(勝手には動かない)」という意味です。disable-model-invocation: trueは「このスキルは(AIモデルによる)自動実行をしない」設定で、ユーザーが明示的に呼び出したときだけ動かすためのフラグです。
Instructions(手順)
あなたはユーザーが Notion Worker 用の新しい sync 機能を作るのを支援する。各ステップを順に進め、質問と推奨を行い、最後に動作するコードを生成する。
開始前に、sync のパターンを理解するため次の参照ファイルを読むこと:
.agents/skills/sync-guide/SKILL.md— 概念、モード、パターン、よくあるミス.agents/skills/sync-guide/api-pagination-patterns.md— 実際の API における戦略.agents/skills/sync-guide/examples/— 動作するコードテンプレート既存の実装を理解するため、現在の
src/index.tsも読むこと。
最初に手順の概要が書かれています。このファイル以外にも pagination についての解説など別の skill もあるようです。これらも確認するように支持されています。
Step 1: データソースを理解する
ユーザーに質問する:
- 何のデータを同期しますか?(例: "Jira issues", "Stripe customers", "ServiceNow tickets")
よく知られた API 名が出た場合は、その API のページネーション方式と変更追跡機能(
updated_atがあるか、events エンドポイントがあるか、カーソルベースか等)を調べる。
まずは最初にユーザに何を作るのかインタビューするようです。昨日の INSTRUCTION に書かれていたように sync の場合には Delta が使えるかどうかはかなり重要なので、相手先の API が変更追跡機能があるかどうかを調べるところまでを実施するようです。
Step 2: 適切なアーキテクチャを決める
データソースの特性に基づき、次の 2 つのどちらかを推奨する。
Simple replace sync(単純な置換同期)
使いどころ: ソースが小さい(<1k レコード) または API に変更追跡がない (
updated_atがない / event feed がない)。
- 1 つの sync(replace モード)で毎回全件再取得する。
- 実行環境が、ソースから消えたレコードを自動削除する(mark-and-sweep)。
- 最もシンプル。
Backfill + delta pair(バックフィル+差分の 2 本立て)
使いどころ: API が変更追跡(
updated_at/ events / changelog)を提供する場合 (Salesforce, Stripe, Linear, GitHub, HubSpot などの多くのエンタープライズ API が該当)。同じデータベースに書き込む 2 つの sync を用意する:
- Backfill sync(replace,
schedule: "manual"): 全件をページングしながら取得。全再取り込みが必要なときに CLI で手動実行。replace の mark-and-sweep により、ソースから削除されたレコードも自動的に掃除される。- Delta sync(incremental,
schedule: "5m"等): 最近変更されたレコードのみ取得。タイマーで回し、低遅延で更新する。単一の「二相(バックフィル+差分)」sync に比べた利点:
- state の「フェーズ判定」が不要(各 sync の state が単純)
- backfill→delta の遷移ロジックが不要
- backfill と delta が独立(いつでも再バックフィル可能)
- 理解・デバッグが容易
判断基準はデータサイズではなく変更追跡の有無。 Linear のように数千件でも delta に必要なクエリが可能なら backfill+delta が適切。逆に、
updated_sinceなどがない API は件数に関係なく単純 replace になる。簡潔な理由付きで推奨案を提示し、ユーザーが異論を唱えた場合は上書きを許容する。
基本的には変更追跡ができれば、Backfill + Delta を選ぶように書かれています。
Step 3: スキーマを設計する
API のレスポンス形状に基づきスキーマ案を提示する。API が返すフィールドを調べ、有用なものを Schema 型へマッピングする。ユーザーにフィールドの列挙を要求しない(デフォルト案を出して調整してもらう)。
例: Jira issues を同期する場合
const issuesDb = worker.database("issuesDb", { type: "managed", initialTitle: "Jira Issues", primaryKeyProperty: "Issue Key", schema: { properties: { "Issue Key": Schema.richText(), // primaryKeyProperty — ユニークID "Summary": Schema.title(), // 主要な表示フィールド "Status": Schema.select([...]), // Jira のステータスに対応 "Assignee": Schema.richText(), // email があれば Schema.people() でも可 "Updated": Schema.date(), }, }, });ガイドライン:
worker.database()で DB を宣言し、worker.sync()ではそのハンドルを参照する- スキーマには
Schema.title()が必ず 1 つ必要(最も説明的なフィールドを選ぶ)- 主キー(ユニーク ID)プロパティには
Schema.richText()を使う- 型が合う場合は
Schema.url()/Schema.email()/Schema.date()/Schema.number()/Schema.checkbox()/Schema.select()を使う- 別の managed DB への関連は
Schema.relation("otherDatabaseKey")を使う- プロパティは 10〜20 個から開始(有用なフィールドは積極的に含める)
- 型一覧は
.agents/skills/sync-guide/SKILL.mdの "Schema Reference" を参照コード生成前に、ユーザーへ「追加・削除・変更したいフィールドがあるか」を確認する。
スキーマの設定は API のレスポンスに合わせて設計します。型については sync-guide/SKILL.md に一覧があるようなので、後日調べてみることにします。relation については昨日も書かれていましたが、これ以上の情報がないので、また別のところに記載があるのかもしれません。
Step 4: ステートマシンを設計する
API のページネーション方式と変更追跡方式を調べて設計する。ページネーション詳細をユーザーに質問しない(API ドキュメント/既知情報/調査で判断する)。
確認事項:
- 一覧取得のページネーション方式(不透明カーソル / ページ番号 / オフセット / キーセット等)
- 変更追跡の有無(
updated_at/ events / changelog 等)- 削除のシグナルの有無(archived フィルタ / 監査ログ / delete events 等)
設計指針:
単純 replace sync: state は 1 サイクル内のページングのみ
- 不透明カーソル:
{ cursor: string | null }- ページ番号:
{ page: number }backfill + delta: sync ごとに単純な state(判別 union の二相 state は不要)
- Backfill state: 全件走査のページング用カーソル(API による)
- 不透明カーソル:
{ cursor: string | null }- ページ番号:
{ page: number }- キーセット:
{ cursorTimestamp: string | null; cursorId: string | null }- Delta state: 変更追跡用カーソル(API による)
- 不透明カーソル(updated_at ソート):
{ cursor: string | null }- タイムスタンプキーセット:
{ cursorTimestamp: string; cursorId: string }- イベントID:
{ eventCursor: string }整合性バッファ(delta のみ): incremental ではカーソルが戻らないため、まだインデックスされていないレコードを飛ばすと永久に取りこぼす。常に「今」から 10〜15 秒以上手前までしかカーソルを進めない(Stripe/Salesforce 等の最終整合性 API で重要)。
削除の扱い: 3 パターン
- delta で delete が取れる(Stripe の
.deleted系 events 等): delta sync で{ type: "delete", key }を出す(最も綺麗)- 削除が稀/重要でない: 何もしない(Notion 側に古いレコードが残るが問題にならない)
- 重要だが delete シグナルがない: backfill の replace(mark-and-sweep)で掃除する。定期的に backfill を回して陳腐化を除去する
変更追跡がない場合は、単純 replace sync の推奨に戻す。
例の形式で state 設計の要約を提示して確認を取る:
「この API はカーソルページネーションで
updated_atがあるため、backfill+delta を採用。backfill は不透明カーソル、delta はタイムスタンプキーセットで実装する」等。
pagination の戦略、削除の戦略を決めるためのステートマシンを設計します。基本的にはユーザには詳しいことは知らせず、AI 側で調査して設計するようになっているようです。
Step 5: 認証をセットアップする
コード生成前に、API が必要とする認証方式を調べ、ローカルテストできるようにセットアップする。
パターンは 2 つ:
Pattern A: 固定 API トークン/キー
個人トークン/ API key を使う API(例: Jira API token, GitHub PAT, シンプル API key)向け。
- ユーザーにトークンを聞き、
.envに追加する:JIRA_API_TOKEN=... JIRA_EMAIL=user@example.com
.envがなければ作成する(--local実行時に自動ロードされる)Pattern B: OAuth
OAuth 必須の API(例: Google, Salesforce, HubSpot)向け。必要要素は 2 種類:
クライアント資格情報(client ID/secret):
.envに入れるMY_OAUTH_CLIENT_ID=... MY_OAUTH_CLIENT_SECRET=...ユーザートークン: デプロイ後 に OAuth フローで取得(runtime が
worker.oauth()と.accessToken()で扱う)OAuth のコードでは
worker.oauth()を追加する。Notion-managed OAuth は alpha の可能性が高いため、{ provider: "..." }の省略形ではなく、エンドポイントを明示するUserManagedOAuthConfigurationを使う。const myAuth = worker.oauth("myAuth", { name: "my-provider", authorizationEndpoint: "<https://provider.example.com/oauth/authorize>", tokenEndpoint: "<https://provider.example.com/oauth/token>", scope: "read write", clientId: process.env.MY_OAUTH_CLIENT_ID ?? "", clientSecret: process.env.MY_OAUTH_CLIENT_SECRET ?? "", });execute 関数では
process.envの固定トークンではなくawait myAuth.accessToken()を使う。注: OAuth sync は、OAuth フローにデプロイ済み Worker が必要なため、ローカルで完全にはテストできない(
.accessToken()で失敗する)。その場合は Step 8(deploy + preview)へ進む。
外部の認証は固定APIキーやトークンを使うか、OAuth を使うことができます。ただし、OAuth についてはコールバックの URL へのアクセスができないといけないので、ローカルでのテストはできません。この場合には実際にデプロイしてプレビューする手順を取るようです。
Step 6: コードを生成する
src/index.tsに sync を実装する。.agents/skills/sync-guide/examples/のうち最も近い例をベースにする:
replace-simple.ts— 静的データ、API なしreplace-paginated.ts— ページングあり replace(backfill にも使う)incremental-basic.ts— 不透明カーソルの deltaincremental-bimodal.ts— backfill + delta のフル例incremental-events.ts— event feed の delta生成コードに含めるもの:
- 適切な import(
Worker,Builder,Schema)worker.database()による DB 宣言(schema とprimaryKeyProperty)- upstream API 用 pacer(
worker.pacer())と、各 API リクエスト前のawait pacer.wait()- state 型(sync ごとに 1 つの単純な型。判別 union 不要)
- DB ハンドルを参照する
worker.sync()呼び出し- backfill+delta の場合: 同じ DB に対し 2 sync、backfill は
schedule: "manual"、delta は定期実行- delta の整合性バッファ(最終整合性 API の場合)
- 設計意図(なぜそうするか)のコメント
fetchによる API コール(認証はprocess.envから)コード生成チェックリスト:
- [ ]
worker.database()で DB 宣言し、ハンドルで参照している- [ ]
worker.pacer()を宣言している- [ ] 各
fetch前にawait pacer.wait()を呼んでいる- [ ] state が単純(判別 union を使っていない)
- [ ] backfill が
mode: "replace"かつschedule: "manual"(該当時)- [ ] delta が
mode: "incremental"かつ定期実行(該当時)- [ ] delta のカーソル更新に整合性バッファがある(該当時)
- [ ] 削除の扱いが Step 4 の 3 ケースに整合している
あとは実際にコードを作成します。上で設計した情報をもとに、example にあるものから近いものを選んでくればよいようです。また、コード内でやるべきことが箇条書きになっていますし、作成したコードのチェック項目も用意されています。AI はこれをベースにセルフテストができるようになっています。
Step 7: ローカルでテストする
デプロイ前にテストし、早期にバグを発見する。
固定トークン(Pattern A)の場合
npm run checkで TypeScript の型チェックを通す(エラーは修正)ntn workers exec <key> --localでローカル実行(.envをロードして execute が動く)
- データが返るか
- プロパティが正しく埋まるか
hasMoreとカーソル進行が妥当かhasMore: trueの場合、次ページをテストする:ntn workers exec <key> --local -d '<previous output の nextState>'- 認証失敗/フィールドマッピング誤り/クラッシュ等があれば修正して再実行(デプロイ不要で反復が速い)
- backfill+delta の場合はそれぞれ別にテスト:
- backfill:
ntn workers exec <backfillKey> --local- delta:
ntn workers exec <deltaKey> --local
test.tsを作り、worker を import して.run()を直接叩くテストを書く。.envに実 API 資格情報があるなら実 API を叩く統合テストが最も有効。なければ HTTP をスタブする。統合テスト例(資格情報がある場合推奨)
import "dotenv/config"; // .env をロード import worker from "./src/index.ts"; import assert from "node:assert"; async function test() { // 1ページ目(初回、state なし) const page1 = await worker.run("mySync", undefined, { concreteOutput: true }); console.log(`Page 1: ${page1.changes.length} records, hasMore: ${page1.hasMore}`); assert(page1.changes.length > 0, "Should return records"); // フィールドが埋まっていることを確認 const first = page1.changes[0]; assert(first.key, "Record should have a key"); console.log("Sample record:", JSON.stringify(first, null, 2)); // ページング確認 if (page1.hasMore) { const page2 = await worker.run("mySync", page1.nextState, { concreteOutput: true }); console.log(`Page 2: ${page2.changes.length} records, hasMore: ${page2.hasMore}`); assert(page2.changes.length > 0, "Second page should return records"); } console.log("All tests passed!"); } test().catch((err) => { console.error(err); process.exit(1); });
npx tsx test.tsで実行し、capability key や検証内容は対象 sync に合わせて調整する(特に backfill+delta の両方を検証する)。OAuth(Pattern B)の場合
.accessToken()はデプロイ済み Worker と OAuth 完了が必要なので、ローカル実行は不可。Step 8(deploy + preview)へ進む。型チェックとしてnpm run checkは実行可能。
固定 API キーやトークンの場合には、ローカルでテストが可能です。backfill + delta の場合には、両方のアクセスを確認するように依頼しています。テスト項目も箇条書きになっているので、順番にテストすればよさそうです。
Step 8: デプロイし、Preview で検証する
ローカルテストが通ったら(OAuth は直ちに)デプロイしてリモートでテストする。
デプロイ時点で secret が必要な場合(例: capability 登録時に
process.envから OAuthclientSecretを読む等)は:
ntn workers create --name <name>— デプロイせず worker 作成ntn workers env push—.envをリモートへ pushntn workers deploy— secret がある状態でデプロイ不要ならシンプルに:
ntn workers deployntn workers env pushOAuth の場合は preview の前に OAuth フローを完了する。重要:
env pushはoauth startより先に行う(client secret がないとトークン交換できない)。
ntn workers oauth show-redirect-url— リダイレクト URL を取得- OAuth プロバイダ側アプリ設定へその URL を登録するようユーザーに案内
ntn workers oauth start <oauthKey>— ブラウザで OAuth 開始Preview 実行:
ntn workers sync trigger <syncKey> --preview— Notion へ書き込まずにリモート実行
- レコード数、プロパティ値、
hasMoreを確認hasMore: trueなら継続:ntn workers sync trigger <syncKey> --preview --context '<nextState>'- 問題があれば修正して再デプロイ(Step 1 に戻る)
backfill+delta の場合は両方 preview:
ntn workers sync trigger <backfillKey> --previewntn workers sync trigger <deltaKey> --preview
ローカルテストが完了するか OAuth の場合にはデプロイしてプレビューテストを実行します。OAuth の場合には、クライアントシークレットを push した後で、OAuth のトークン交換を実施します。その後は --preview で Notion の書き込みをしないテストを実行します。backfill と delta の場合には、両方とも preview テストを実行します。
Step 9: 本番実行(Go Live)
Preview が良ければ:
ntn workers sync trigger <key>— 本番同期を開始ntn workers sync status— 実行中/進捗確認ntn workers runs list→ntn workers runs logs <runId>— エラー確認- 再度
ntn workers sync statusで進捗を確認(件数増加、エラーなし)backfill+delta の場合:
ntn workers sync trigger <backfillKey>— まず全件 backfillntn workers sync statusで完了まで監視- 以後は delta がスケジュールで自動実行
ユーザーへの案内: 初回は backfill で時間がかかる可能性がある。完了まで
ntn workers sync statusで監視する。以後は delta が自動実行。再 backfill する場合は:ntn workers sync state reset <backfillKey> && ntn workers sync trigger <backfillKey>
preview で問題がなければ、本番を実行します。backfill + delta の場合には、初回かなり時間がかかるはずなので、status で監視して初回同期終了を確認します。その後は指定された時間ごとに delta の差分同期が実行されているかを確認します。
おわりに
AI に対する Skill は本当によいレシピですね。こういう SKILL を自分で書けるようになれば自分でコードを書かなくてもいいんですよね。これからはそういう時代になるということですね。
https://hkob.notion.site/hkob-16dd8e4e98ab807cbe3cf3cc94cdfe0f?pvs=4hkob.notion.site