<aside> <img src="/icons/square-three-fourths_gray.svg" alt="/icons/square-three-fourths_gray.svg" width="40px" />

Duplicate this Skill into your workspace so start using it

</aside>

Screenshot 2026-09-30 at 10.54.07 AM.png

Use this when building an Asana → Notion sync as a Notion Worker. It reproduces a proven setup: two related managed databases (Projects and Tasks), native Notion row icons carried over from Asana, person properties for assignees and owners, and hourly replace syncs.

Not for: writing back to Asana (this sync is one-way), or one-off Asana imports that don't need to stay current.

Before you start

1. Scaffold

ntn workers new asana-project-sync
cd asana-project-sync && npm install

Read .agents/skills/sync-guide/ (and api-pagination-patterns.md) before writing code. Asana uses offset pagination, which is covered there.

2. Capabilities used

Handle Type Purpose
ASANA_PAT worker.credential() Bearer token injected into app.asana.com requests
asanaProjects worker.database() Managed Projects target
asanaTasks worker.database() Managed Tasks target, related to projects
asanaApi worker.pacer() 120 requests / 60s (Asana's limit is 150/min)
asanaProjectsSync worker.sync() Hourly, replace mode
asanaTasksSync worker.sync() Hourly, replace mode

3. Authentication

Declare the PAT as a credential, not a plaintext secret, so worker code can never read the raw value:

worker.credential("ASANA_PAT", {
  network: [
    {
      domain: "app.asana.com",
      transform: [{ headers: { Authorization: `Bearer ${CREDENTIAL_VALUE}` } }],
    },
  ],
})

Project selection is ordinary config. Support both a comma-separated list and numbered slots, then merge and dedupe:

process.env.ASANA_PROJECT_GIDS          // "123,456,789"
process.env.ASANA_PROJECT_GID_1         // one project per variable

Throw if no GIDs resolve — an empty list plus replace mode would wipe both databases.

4. Databases