Read and update Asana workspaces, projects, tasks, and stories through OAuth or a PAT.
- Other
- asana
- workspaces
- projects
- tasks
- stories
- comments
- oauth
- pat
- License
- MIT
- Published
- August 22, 2026
- Pinned commit
c016c50- Rating
- No ratings yet
- Forks
- 0
- Stars
- 0
- Adaptation effort
- —
README
@kody/asana
Intent
Provide reusable, account-agnostic Asana helpers so Kody agents can read and
update workspaces, projects, tasks, and stories/comments through a saved
asana / asana-* OAuth integration or a personal access token — without
hand-rolling REST. Mutations are previewable with dryRun: true and only
run live after confirm: true.
This listing is meant to be forked. After you fork, connect your Asana
workspace. Do not treat the live @kody/asana package storage as yours.
When To Use
- List or inspect workspaces, projects, tasks, and stories/comments
- Create or update projects, tasks, and comments after explicit confirmation
- Call an unwrapped Asana REST path through
./request - Connect more than one Asana account via
integrationName/account
Auth
Asana has no built-in Kody OAuth app. Choose one lane:
| Lane | When to use | Credential |
|---|---|---|
| OAuth (recommended) | Multi-account, refresh tokens, shared helpers | Saved integration asana or asana-<purpose> |
| Personal access token | Fastest for a single workspace / personal scripts | User secret asanaPat (or asanaPat-<purpose>) |
Both lanes send Authorization: Bearer …. Required API host:
app.asana.com. Approve it in the account secrets UI.
Lane A — BYO OAuth
- Create an OAuth application at https://app.asana.com/0/my-apps
- Set the redirect URI exactly to
https://kody.codes/connect/oauth - Under OAuth → Permission scopes, enable the scopes this package uses
(
workspaces:read,projects:read,projects:write,tasks:read,tasks:write,stories:read,stories:write,users:read) or toggle Full permissions (default). - Connect while signed in to Kody:
- Paste the Asana client id and client secret into the Kody wizard (never into
chat). Approve host
app.asana.com. - Reconnect later with https://kody.codes/connect/oauth?provider=asana
OAuth authorize URL: https://app.asana.com/-/oauth_authorize. Token URL:
https://app.asana.com/-/oauth_token. Flow: confidential (client secret) plus
S256 PKCE. Asana scopes are space-separated.
To connect a second workspace, change provider (for example
provider=asana-work) and pass integrationName: 'asana-work' on every call.
Lane B — Personal access token
- Create a token at https://app.asana.com/0/my-apps
- Save it (do not paste the value in chat):
For a second workspace/token, use a distinct secret name such as
asanaPat-work and pass secretName: 'asanaPat-work' (or
account: 'work', which resolves to asana-work / asanaPat-work).
Scopes
| Scope | Needed for |
|---|---|
workspaces:read | ./list-workspaces, ./get-workspace |
projects:read | ./list-projects, ./get-project |
projects:write | ./create-project |
tasks:read | ./list-tasks, ./get-task |
tasks:write | ./create-task, ./update-task |
stories:read | ./list-stories |
stories:write | ./create-story |
users:read | ./viewer, ./smoke-test |
default | Full permissions when an endpoint has no granular scope |
If Asana returns 401/403 or an insufficient-scope error, helpers throw a message that names the missing scope and the next setup URL (reconnect OAuth with that scope, or save a PAT).
Multiple accounts
Every export accepts:
integrationName/integration— exact saved OAuth name (asana-work)account—work→asana-work;asana-workused as-is; omitted →asanasecretName— PAT secret overrideauth—'oauth'or'pat'when both exist
Do not hard-code a personal workspace GID or alias.
Safety
Mutating helpers require confirm: true. Pass dryRun: true to inspect the
REST payload without calling Asana. ./request treats GET as read-only;
POST / PUT / PATCH / DELETE need confirmation.
Exports
| Export | Description |
|---|---|
. | Package overview, connect URLs, export map |
./accounts | Resolve integration/secret names and report what is connected |
./smoke-test | Local helper checks plus optional live viewer read (no email) |
./viewer | Authenticated user gid / name |
./list-workspaces | List workspaces |
./get-workspace | Get one workspace by GID |
./list-projects | List projects (workspaceGid, teamGid, archived) |
./get-project | Get one project by GID |
./create-project | Preview or create a project (workspaceGid or teamGid) |
./list-tasks | List tasks (projectGid, or assignee + workspaceGid) |
./get-task | Get one task by GID |
./create-task | Preview or create a task (workspaceGid or projects) |
./update-task | Preview or update a task |
./list-stories | List stories/comments on a task |
./create-story | Preview or create a comment on a task |
./request | Generic REST escape hatch |
./types | Shared TypeScript types |
Smoke test
import smokeTest from 'kody:@kody/asana/smoke-test'
export default async function main() {
return await smokeTest()
}Without credentials this returns { ok: true, live: false } plus the connect
and PAT URLs. After OAuth or a PAT is saved it reads /users/me and returns
{ live: true, hasViewerGid } without email.
Preview a mutation without credentials:
import createTask from 'kody:@kody/asana/create-task'
export default async function main() {
return await createTask({
name: 'Follow up on onboarding',
workspaceGid: '1234567890',
dryRun: true,
})
}Examples
import listWorkspaces from 'kody:@kody/asana/list-workspaces'
import listProjects from 'kody:@kody/asana/list-projects'
export default async function main() {
const { items: workspaces } = await listWorkspaces()
return await listProjects({
workspaceGid: workspaces[0]?.gid,
limit: 20,
})
}import createTask from 'kody:@kody/asana/create-task'
export default async function main() {
const preview = await createTask({
name: 'Investigate checkout timeout',
workspaceGid: '123',
assignee: 'me',
dryRun: true,
})
// After the user confirms the exact workspace and title:
return await createTask({
name: 'Investigate checkout timeout',
workspaceGid: '123',
assignee: 'me',
confirm: true,
})
}Unwrapped REST:
import request from 'kody:@kody/asana/request'
export default async function main() {
return await request({
path: '/users/me',
query: { opt_fields: 'gid,name' },
})
}Notes
- REST base:
https://app.asana.com/api/1.0 - Helpers project slim objects with
opt_fields. Use./requestwhen you need extra fields. assignee: 'me'on./list-tasks/./create-taskis the authorizing user.- List tasks requires
projectGid, or bothassigneeandworkspaceGid. - This package is not affiliated with or endorsed by Asana, Inc.
Branding
The community icon is Asana's official three-dot logomark from Asana brand (the coral mark published in the horizontal lockup). Paths are unmodified. Asana® is a trademark of Asana, Inc.
Docs
Report this listing
Log in to report this listing.