Asana Integration Skill
Integration patterns for syncing development sessions with Asana tasks.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ ASANA INTEGRATION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Claude │────►│ REST API │────►│ Asana │ │
│ │ Session │◄────│ Client │◄────│ Cloud │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Session │ │ Webhooks │ │
│ │ Files │ │ │ │
│ └─────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
When to Use This Skill
- Setting up Asana integration for the first time
- Syncing development sessions with Asana tasks
- Creating tasks from Claude sessions
- Updating task completion status
- Managing projects and sections
Configuration
.claude/config/workspace.json
{
"taskManager": {
"enabled": true,
"provider": "asana",
"syncWithSession": true,
"autoUpdateStatus": true,
"config": {
"accessToken": "${ASANA_ACCESS_TOKEN}",
"defaultWorkspace": "your-workspace-gid",
"defaultProject": "your-project-gid"
}
}
}
Environment Variables
# .env.local
ASANA_ACCESS_TOKEN=1/12345678901234:abcdefghijklmnop...
Authentication
Personal Access Token (PAT)
// Bearer token authentication
const headers = {
'Authorization': `Bearer ${process.env.ASANA_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// Base URL
const BASE_URL = 'https://app.asana.com/api/1.0';
OAuth 2.0
// For OAuth apps
const authUrl = 'https://app.asana.com/-/oauth_authorize';
const tokenUrl = 'https://app.asana.com/-/oauth_token';
// Exchange code for token
const response = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.ASANA_CLIENT_ID,
client_secret: process.env.ASANA_CLIENT_SECRET,
redirect_uri: 'https://your-app.com/callback',
code: authorizationCode
})
});
API Patterns
Common Endpoints
| Endpoint |
Method |
Description |
/users/me |
GET |
Get current user |
/workspaces |
GET |
List workspaces |
/projects |
GET |
List projects |
/projects/{gid} |
GET |
Get project details |
/projects/{gid}/tasks |
GET |
List tasks in project |
/tasks |
POST |
Create task |
/tasks/{gid} |
GET/PUT/DELETE |
Task operations |
/tasks/{gid}/stories |
GET/POST |
Task comments/activity |
/sections |
GET |
List sections |
Task Structure
interface AsanaTask {
gid: string;
name: string;
notes: string; // Plain text description
html_notes: string; // HTML description
completed: boolean;
completed_at: string | null;
due_on: string | null; // YYYY-MM-DD
due_at: string | null; // ISO datetime
start_on: string | null;
assignee: {
gid: string;
name: string;
email: string;
} | null;
projects: Array<{
gid: string;
name: string;
}>;
memberships: Array<{
project: { gid: string; name: string };
section: { gid: string; name: string };
}>;
tags: Array<{
gid: string;
name: string;
color: string;
}>;
custom_fields: Array<{
gid: string;
name: string;
type: string;
enum_value?: { gid: string; name: string };
number_value?: number;
text_value?: string;
}>;
permalink_url: string;
}
Session Sync Patterns
Linking Session to Task
# In requirements.md
## Asana Task
- **Task GID:** 1234567890123456
- **URL:** https://app.asana.com/0/project-gid/1234567890123456
- **Status:** In Progress (via section)
Task Management
Asana uses sections for status (unlike status fields):
// Move task to section (change status)
await fetch(`${BASE_URL}/sections/${sectionGid}/addTask`, {
method: 'POST',
headers,
body: JSON.stringify({
data: { task: taskGid }
})
});
// Mark task complete
await fetch(`${BASE_URL}/tasks/${taskGid}`, {
method: 'PUT',
headers,
body: JSON.stringify({
data: { completed: true }
})
});
Section Mapping
| Session Event |
Asana Action |
| Session created |
Move to "In Progress" section |
| Blocked |
Move to "Blocked" section |
| Session closed (success) |
Mark completed |
| Session closed (partial) |
Move to "Review" section |
Adding Stories (Comments)
// Add comment to task
await fetch(`${BASE_URL}/tasks/${taskGid}/stories`, {
method: 'POST',
headers,
body: JSON.stringify({
data: {
text: `
## Session Progress Update
**Phase:** Backend Development
**Progress:** 60%
### Completed
- API endpoints created
- Tests written
### In Progress
- Frontend components
`.trim()
}
})
});
Webhook Configuration
Creating Webhooks
// Create webhook
const webhook = await fetch(`${BASE_URL}/webhooks`, {
method: 'POST',
headers,
body: JSON.stringify({
data: {
resource: projectGid, // What to watch
target: 'https://your-app.com/api/webhooks/asana',
filters: [
{ resource_type: 'task', action: 'changed' },
{ resource_type: 'task', action: 'added' },
{ resource_type: 'story', action: 'added' }
]
}
})
});
Webhook Handshake
Asana requires a handshake to verify webhook endpoint:
// app/api/webhooks/asana/route.ts
export async function POST(request: Request) {
// Handle handshake
const hookSecret = request.headers.get('X-Hook-Secret');
if (hookSecret) {
return new Response(null, {
status: 200,
headers: { 'X-Hook-Secret': hookSecret }
});
}
// Verify signature
const signature = request.headers.get('X-Hook-Signature');
const body = await request.text();
// Verify HMAC-SHA256 signature...
const payload = JSON.parse(body);
for (const event of payload.events) {
switch (event.action) {
case 'changed':
// Handle task update
break;
case 'added':
// Handle new task/comment
break;
}
}
return Response.json({ received: true });
}
Event Types
| Resource |
Action |
Trigger |
| task |
added |
Task created |
| task |
changed |
Task updated |
| task |
removed |
Task deleted |
| task |
undeleted |
Task restored |
| story |
added |
Comment/activity added |
Custom Fields
Reading Custom Fields
// Get task with custom fields
const response = await fetch(
`${BASE_URL}/tasks/${taskGid}?opt_fields=custom_fields,custom_fields.name,custom_fields.enum_value`,
{ headers }
);
const { data: task } = await response.json();
// Find specific field
const statusField = task.custom_fields.find(f => f.name === 'Status');
console.log(statusField.enum_value?.name); // "In Progress"
Updating Custom Fields
// Update custom field value
await fetch(`${BASE_URL}/tasks/${taskGid}`, {
method: 'PUT',
headers,
body: JSON.stringify({
data: {
custom_fields: {
[customFieldGid]: enumOptionGid // For enum fields
// or
[customFieldGid]: 42 // For number fields
// or
[customFieldGid]: "text value" // For text fields
}
}
})
});
Rate Limiting
Limits
| Type |
Limit |
| Standard |
1500 requests/minute |
| Free tier |
150 requests/minute |
| Per-user |
Based on plan |
Headers
// Asana uses retry-after for rate limits
const retryAfter = response.headers.get('Retry-After');
if (response.status === 429) {
await sleep(parseInt(retryAfter) * 1000);
// Retry request
}
Error Handling
Common Error Codes
| Code |
Description |
Resolution |
| 400 |
Bad request |
Check request body |
| 401 |
Unauthorized |
Check token |
| 403 |
Forbidden |
Check permissions |
| 404 |
Not found |
Verify GID |
| 429 |
Rate limited |
Wait and retry |
| 500 |
Server error |
Retry with backoff |
Error Response Format
{
"errors": [
{
"message": "task: Unknown object: 1234567890",
"help": "For more information on API status codes..."
}
]
}
Testing Patterns
Mock API Responses
// __mocks__/asana.ts
export const mockTask = {
gid: "1234567890123456",
name: "Test Task",
completed: false,
assignee: { gid: "user-gid", name: "Test User" }
};
export const mockGetTask = jest.fn().mockResolvedValue({ data: mockTask });
Integration Tests
describe('Asana Integration', () => {
it('should fetch task details', async () => {
const task = await asana.getTask('1234567890123456');
expect(task.name).toBeDefined();
});
it('should complete task', async () => {
await asana.completeTask('1234567890123456');
const task = await asana.getTask('1234567890123456');
expect(task.completed).toBe(true);
});
});
Anti-Patterns
DON'T: Forget opt_fields
// BAD - Returns minimal data
const task = await fetch(`${BASE_URL}/tasks/${gid}`);
// GOOD - Request needed fields
const task = await fetch(
`${BASE_URL}/tasks/${gid}?opt_fields=name,notes,assignee,completed,custom_fields`
);
DON'T: Poll for Changes
// BAD - Polling wastes resources
setInterval(async () => {
const tasks = await getTasks();
// Check for changes...
}, 5000);
// GOOD - Use webhooks
await createWebhook(projectGid, callbackUrl);
Checklist
Before using Asana integration:
Related Skills
session-management - Session lifecycle
scheduled-actions - Background task processing
service-layer - API implementation patterns