Asana ChatGPT Integration: Complete Task Automation Guide

Asana's powerful REST API enables you to build sophisticated task management integrations for ChatGPT apps. Whether you're automating project workflows, syncing team tasks, or creating custom reporting dashboards, Asana's API provides comprehensive access to workspaces, projects, tasks, custom fields, portfolios, and real-time webhooks.

This production-ready guide demonstrates how to integrate Asana with ChatGPT apps using TypeScript, covering OAuth 2.0 authentication, task and project management, custom field manipulation, webhook event processing, portfolio tracking, and advanced automation patterns. By the end, you'll have a complete Asana integration toolkit that enables conversational task management through ChatGPT.

Asana's API supports complex organizational structures with workspaces, teams, projects, sections, tags, and custom fields. The platform's webhook system provides real-time updates for task changes, project modifications, and status updates. With proper OAuth implementation and rate limit handling, you can build enterprise-grade integrations that scale across thousands of tasks and hundreds of projects.

This guide provides 10 production-ready TypeScript implementations covering authentication, CRUD operations, advanced features, webhook processing, and analytics. Each example includes error handling, retry logic, pagination support, and type safety. Whether you're building a simple task creator or a comprehensive project management automation system, these patterns will accelerate your development.

For broader API integration strategies, see our API Integration Best Practices for ChatGPT Apps guide. If you're building no-code solutions, explore MakeAIHQ's ChatGPT App Builder platform.

Asana API Setup and OAuth Authentication

Asana uses OAuth 2.0 for secure authentication, supporting both authorization code flow (for user-facing apps) and personal access tokens (for server-to-server integrations). The API is organized around REST principles with JSON request/response formats, comprehensive error codes, and rate limiting at 1,500 requests per minute per workspace.

Setting up Asana authentication requires registering an OAuth application in the Asana Developer Console, configuring redirect URIs for the authorization callback, and implementing the three-legged OAuth flow. The API returns access tokens valid for one hour, with refresh tokens that enable long-lived integrations without repeated user authorization.

Asana's workspace model requires selecting a workspace context for most API operations. Users can belong to multiple workspaces, each with distinct teams, projects, and custom field configurations. Your integration must handle workspace selection gracefully, especially when users have access to multiple organizations.

Rate limiting is enforced per workspace with a 1,500 request per minute limit. The API returns Retry-After headers when rate limits are exceeded, and implements exponential backoff recommendations. For high-volume integrations, consider implementing request batching and caching strategies to stay within limits.

1. Asana OAuth Client (TypeScript)

import axios, { AxiosInstance } from 'axios';
import crypto from 'crypto';

interface AsanaConfig {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
  scopes?: string[];
}

interface TokenResponse {
  access_token: string;
  refresh_token: string;
  expires_in: number;
  token_type: string;
}

interface AsanaTokens {
  accessToken: string;
  refreshToken: string;
  expiresAt: Date;
}

export class AsanaOAuthClient {
  private config: AsanaConfig;
  private client: AxiosInstance;
  private tokens: AsanaTokens | null = null;

  constructor(config: AsanaConfig) {
    this.config = {
      ...config,
      scopes: config.scopes || ['default']
    };

    this.client = axios.create({
      baseURL: 'https://app.asana.com/api/1.0',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      timeout: 30000
    });

    this.setupInterceptors();
  }

  private setupInterceptors(): void {
    // Request interceptor: Add auth headers
    this.client.interceptors.request.use(
      async (config) => {
        if (this.tokens) {
          // Check if token is expired
          if (new Date() >= this.tokens.expiresAt) {
            await this.refreshAccessToken();
          }

          config.headers.Authorization = `Bearer ${this.tokens.accessToken}`;
        }

        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor: Handle rate limiting
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        if (error.response?.status === 429) {
          const retryAfter = parseInt(error.response.headers['retry-after'] || '60');
          console.warn(`Rate limited. Retrying after ${retryAfter}s`);

          await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
          return this.client.request(error.config);
        }

        if (error.response?.status === 401 && this.tokens) {
          // Try refreshing token
          await this.refreshAccessToken();
          return this.client.request(error.config);
        }

        return Promise.reject(error);
      }
    );
  }

  public generateAuthUrl(state?: string): string {
    const stateParam = state || crypto.randomBytes(16).toString('hex');
    const params = new URLSearchParams({
      client_id: this.config.clientId,
      redirect_uri: this.config.redirectUri,
      response_type: 'code',
      state: stateParam,
      scope: this.config.scopes!.join(' ')
    });

    return `https://app.asana.com/-/oauth_authorize?${params.toString()}`;
  }

  public async exchangeCodeForToken(code: string): Promise<AsanaTokens> {
    try {
      const response = await axios.post<{ data: TokenResponse }>(
        'https://app.asana.com/-/oauth_token',
        {
          grant_type: 'authorization_code',
          client_id: this.config.clientId,
          client_secret: this.config.clientSecret,
          redirect_uri: this.config.redirectUri,
          code
        },
        {
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
        }
      );

      this.tokens = {
        accessToken: response.data.data.access_token,
        refreshToken: response.data.data.refresh_token,
        expiresAt: new Date(Date.now() + response.data.data.expires_in * 1000)
      };

      return this.tokens;
    } catch (error: any) {
      throw new Error(`Failed to exchange code: ${error.response?.data?.error || error.message}`);
    }
  }

  private async refreshAccessToken(): Promise<void> {
    if (!this.tokens?.refreshToken) {
      throw new Error('No refresh token available');
    }

    try {
      const response = await axios.post<{ data: TokenResponse }>(
        'https://app.asana.com/-/oauth_token',
        {
          grant_type: 'refresh_token',
          client_id: this.config.clientId,
          client_secret: this.config.clientSecret,
          refresh_token: this.tokens.refreshToken
        },
        {
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
        }
      );

      this.tokens = {
        accessToken: response.data.data.access_token,
        refreshToken: response.data.data.refresh_token,
        expiresAt: new Date(Date.now() + response.data.data.expires_in * 1000)
      };
    } catch (error: any) {
      this.tokens = null;
      throw new Error(`Failed to refresh token: ${error.response?.data?.error || error.message}`);
    }
  }

  public setTokens(tokens: AsanaTokens): void {
    this.tokens = tokens;
  }

  public getClient(): AxiosInstance {
    return this.client;
  }

  public isAuthenticated(): boolean {
    return this.tokens !== null && new Date() < this.tokens.expiresAt;
  }
}

For production deployments, see our Deploying ChatGPT Apps to Production guide.

Task and Project Management Operations

Asana's task model supports hierarchical organization with projects, sections, tags, assignees, due dates, custom fields, attachments, and comments. Tasks can belong to multiple projects, have dependencies on other tasks, and include subtasks for breaking down complex work.

Creating tasks requires specifying a workspace context and at least a task name. Optional fields include assignee, due date, notes (description), projects, tags, and custom field values. The API returns a comprehensive task object with metadata including creation time, modification time, permalink URL, and workspace information.

Project management operations enable creating projects, adding sections for task organization, managing project members, and configuring project-level settings like color, layout (list vs. board), and privacy. Projects can be organized into portfolios for high-level tracking across multiple initiatives.

The API supports batch operations for efficient task creation and updates. When creating multiple tasks, consider using the tasks/create endpoint with multiple requests in parallel (respecting rate limits) rather than sequential calls for better performance.

2. Task Manager (TypeScript)

import { AxiosInstance } from 'axios';

interface TaskCreateParams {
  name: string;
  workspace: string;
  assignee?: string;
  assignee_status?: 'inbox' | 'upcoming' | 'later' | 'today';
  completed?: boolean;
  due_at?: string; // ISO 8601
  due_on?: string; // YYYY-MM-DD
  notes?: string;
  projects?: string[];
  tags?: string[];
  custom_fields?: Record<string, any>;
  followers?: string[];
  parent?: string; // For subtasks
}

interface TaskUpdateParams {
  name?: string;
  assignee?: string;
  completed?: boolean;
  due_on?: string;
  notes?: string;
  custom_fields?: Record<string, any>;
}

interface AsanaTask {
  gid: string;
  name: string;
  assignee: { gid: string; name: string } | null;
  completed: boolean;
  due_on: string | null;
  notes: string;
  projects: Array<{ gid: string; name: string }>;
  workspace: { gid: string; name: string };
  permalink_url: string;
  created_at: string;
  modified_at: string;
}

export class AsanaTaskManager {
  constructor(private client: AxiosInstance) {}

  public async createTask(params: TaskCreateParams): Promise<AsanaTask> {
    try {
      const response = await this.client.post('/tasks', {
        data: params
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create task: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getTask(taskId: string, optFields?: string[]): Promise<AsanaTask> {
    try {
      const params = optFields ? { opt_fields: optFields.join(',') } : {};
      const response = await this.client.get(`/tasks/${taskId}`, { params });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get task: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async updateTask(taskId: string, updates: TaskUpdateParams): Promise<AsanaTask> {
    try {
      const response = await this.client.put(`/tasks/${taskId}`, {
        data: updates
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to update task: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async deleteTask(taskId: string): Promise<void> {
    try {
      await this.client.delete(`/tasks/${taskId}`);
    } catch (error: any) {
      throw new Error(`Failed to delete task: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async addTaskToProject(taskId: string, projectId: string, section?: string): Promise<void> {
    try {
      const data: any = { project: projectId };
      if (section) data.section = section;

      await this.client.post(`/tasks/${taskId}/addProject`, { data });
    } catch (error: any) {
      throw new Error(`Failed to add task to project: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getTasksForProject(projectId: string, completed?: boolean): Promise<AsanaTask[]> {
    try {
      const params: any = { opt_fields: 'name,completed,due_on,assignee.name' };
      if (completed !== undefined) params.completed_since = completed ? 'now' : null;

      const response = await this.client.get(`/projects/${projectId}/tasks`, { params });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get project tasks: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async createSubtask(parentTaskId: string, params: Omit<TaskCreateParams, 'workspace'>): Promise<AsanaTask> {
    try {
      const response = await this.client.post(`/tasks/${parentTaskId}/subtasks`, {
        data: params
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create subtask: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async addFollowers(taskId: string, followers: string[]): Promise<AsanaTask> {
    try {
      const response = await this.client.post(`/tasks/${taskId}/addFollowers`, {
        data: { followers }
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to add followers: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }
}

Learn more about REST API Best Practices for ChatGPT Apps.

3. Project Manager (TypeScript)

interface ProjectCreateParams {
  workspace: string;
  name: string;
  notes?: string;
  color?: 'dark-pink' | 'dark-green' | 'dark-blue' | 'dark-red' | 'dark-teal' | 'dark-brown' | 'dark-orange' | 'dark-purple' | 'dark-warm-gray' | 'light-pink' | 'light-green' | 'light-blue' | 'light-red' | 'light-teal' | 'light-brown' | 'light-orange' | 'light-purple' | 'light-warm-gray';
  layout?: 'board' | 'list' | 'timeline' | 'calendar';
  public?: boolean;
  team?: string;
  owner?: string;
  start_on?: string; // YYYY-MM-DD
  due_on?: string; // YYYY-MM-DD
  custom_fields?: string[];
}

interface AsanaProject {
  gid: string;
  name: string;
  notes: string;
  color: string;
  workspace: { gid: string; name: string };
  team: { gid: string; name: string } | null;
  permalink_url: string;
  created_at: string;
  modified_at: string;
}

interface AsanaSection {
  gid: string;
  name: string;
  project: { gid: string; name: string };
  created_at: string;
}

export class AsanaProjectManager {
  constructor(private client: AxiosInstance) {}

  public async createProject(params: ProjectCreateParams): Promise<AsanaProject> {
    try {
      const response = await this.client.post('/projects', {
        data: params
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create project: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getProject(projectId: string): Promise<AsanaProject> {
    try {
      const response = await this.client.get(`/projects/${projectId}`);
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get project: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async updateProject(projectId: string, updates: Partial<ProjectCreateParams>): Promise<AsanaProject> {
    try {
      const response = await this.client.put(`/projects/${projectId}`, {
        data: updates
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to update project: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async deleteProject(projectId: string): Promise<void> {
    try {
      await this.client.delete(`/projects/${projectId}`);
    } catch (error: any) {
      throw new Error(`Failed to delete project: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async createSection(projectId: string, name: string, insertBefore?: string): Promise<AsanaSection> {
    try {
      const data: any = { name };
      if (insertBefore) data.insert_before = insertBefore;

      const response = await this.client.post(`/projects/${projectId}/sections`, {
        data
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create section: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getSections(projectId: string): Promise<AsanaSection[]> {
    try {
      const response = await this.client.get(`/projects/${projectId}/sections`);
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get sections: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async addMembersToProject(projectId: string, members: string[]): Promise<AsanaProject> {
    try {
      const response = await this.client.post(`/projects/${projectId}/addMembers`, {
        data: { members }
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to add members: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async duplicateProject(projectId: string, name: string, include?: string[]): Promise<AsanaProject> {
    try {
      const data: any = { name };
      if (include) data.include = include.join(',');

      const response = await this.client.post(`/projects/${projectId}/duplicate`, {
        data
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to duplicate project: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }
}

For project organization patterns, see Building Multi-Tool ChatGPT Apps.

Advanced Features: Custom Fields and Attachments

Asana's custom fields enable extending task and project metadata beyond standard properties. Custom fields can be text, number, dropdown (enum), multi-select, date, or people fields. Each workspace defines available custom field definitions that can be attached to projects and set on individual tasks.

Working with custom fields requires first querying the workspace's custom field definitions, then setting values on tasks using the custom field GID and appropriate value format. Enum fields use enum option GIDs rather than display text, requiring lookup mappings for user-friendly integrations.

Attachments support uploading files directly to tasks or linking external resources like Google Drive documents, Dropbox files, or web URLs. File uploads use multipart form data with a maximum size of 100MB per attachment. The API returns attachment metadata including download URLs that expire after a period.

Stories represent the activity stream on tasks, including comments, status updates, and system-generated events like task assignments or completions. Creating stories (comments) enables conversational collaboration, while querying stories provides audit trails and activity history.

4. Custom Field Handler (TypeScript)

interface CustomFieldDefinition {
  gid: string;
  name: string;
  resource_type: 'custom_field';
  type: 'text' | 'number' | 'enum' | 'multi_enum' | 'date' | 'people';
  enum_options?: Array<{ gid: string; name: string; enabled: boolean; color?: string }>;
  precision?: number;
  format?: 'currency' | 'percentage' | 'custom' | 'none';
}

interface CustomFieldValue {
  gid: string;
  type: string;
  text_value?: string;
  number_value?: number;
  enum_value?: { gid: string; name: string };
  multi_enum_values?: Array<{ gid: string; name: string }>;
  date_value?: { date: string };
  people_value?: Array<{ gid: string; name: string }>;
}

export class AsanaCustomFieldHandler {
  constructor(private client: AxiosInstance) {}

  public async getCustomFieldsForWorkspace(workspaceId: string): Promise<CustomFieldDefinition[]> {
    try {
      const response = await this.client.get(`/workspaces/${workspaceId}/custom_fields`);
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get custom fields: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async createCustomField(workspaceId: string, params: {
    name: string;
    type: CustomFieldDefinition['type'];
    precision?: number;
    enum_options?: Array<{ name: string; color?: string }>;
  }): Promise<CustomFieldDefinition> {
    try {
      const response = await this.client.post('/custom_fields', {
        data: {
          workspace: workspaceId,
          ...params
        }
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create custom field: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async addCustomFieldToProject(projectId: string, customFieldId: string, insertBefore?: string): Promise<void> {
    try {
      const data: any = { custom_field: customFieldId };
      if (insertBefore) data.insert_before = insertBefore;

      await this.client.post(`/projects/${projectId}/addCustomFieldSetting`, {
        data
      });
    } catch (error: any) {
      throw new Error(`Failed to add custom field: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async setTaskCustomFieldValue(
    taskId: string,
    customFieldId: string,
    value: string | number | string[] | null
  ): Promise<void> {
    try {
      const customFields: Record<string, any> = {};

      if (value === null) {
        customFields[customFieldId] = null;
      } else if (Array.isArray(value)) {
        customFields[customFieldId] = value; // Multi-enum values (array of GIDs)
      } else if (typeof value === 'string') {
        // Could be text, enum GID, or date
        customFields[customFieldId] = value;
      } else if (typeof value === 'number') {
        customFields[customFieldId] = value;
      }

      await this.client.put(`/tasks/${taskId}`, {
        data: { custom_fields: customFields }
      });
    } catch (error: any) {
      throw new Error(`Failed to set custom field: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getTaskCustomFields(taskId: string): Promise<CustomFieldValue[]> {
    try {
      const response = await this.client.get(`/tasks/${taskId}`, {
        params: { opt_fields: 'custom_fields.type,custom_fields.text_value,custom_fields.number_value,custom_fields.enum_value,custom_fields.multi_enum_values,custom_fields.date_value' }
      });

      return response.data.data.custom_fields || [];
    } catch (error: any) {
      throw new Error(`Failed to get task custom fields: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }
}

Explore Custom Field Patterns for SaaS Integrations.

5. Attachment Uploader (TypeScript)

import FormData from 'form-data';
import fs from 'fs';
import { Readable } from 'stream';

interface AsanaAttachment {
  gid: string;
  name: string;
  resource_type: 'attachment';
  download_url: string | null;
  permanent_url: string;
  host: 'asana' | 'dropbox' | 'gdrive' | 'box' | 'external';
  parent: { gid: string; name: string };
  view_url: string;
  created_at: string;
  size?: number;
}

export class AsanaAttachmentUploader {
  constructor(private client: AxiosInstance) {}

  public async uploadAttachment(
    taskId: string,
    file: { name: string; content: Buffer | Readable; contentType: string }
  ): Promise<AsanaAttachment> {
    try {
      const formData = new FormData();
      formData.append('file', file.content, {
        filename: file.name,
        contentType: file.contentType
      });

      const response = await this.client.post(`/tasks/${taskId}/attachments`, formData, {
        headers: {
          ...formData.getHeaders(),
        },
        maxBodyLength: 100 * 1024 * 1024, // 100MB limit
        maxContentLength: 100 * 1024 * 1024
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to upload attachment: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async uploadAttachmentFromPath(taskId: string, filePath: string): Promise<AsanaAttachment> {
    const fileStream = fs.createReadStream(filePath);
    const fileName = filePath.split('/').pop() || 'file';
    const contentType = this.getContentType(fileName);

    return this.uploadAttachment(taskId, {
      name: fileName,
      content: fileStream,
      contentType
    });
  }

  public async createExternalAttachment(
    taskId: string,
    url: string,
    name?: string
  ): Promise<AsanaAttachment> {
    try {
      const response = await this.client.post(`/tasks/${taskId}/attachments`, {
        data: {
          url,
          name: name || url
        }
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create external attachment: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getAttachments(taskId: string): Promise<AsanaAttachment[]> {
    try {
      const response = await this.client.get(`/tasks/${taskId}/attachments`);
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get attachments: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async deleteAttachment(attachmentId: string): Promise<void> {
    try {
      await this.client.delete(`/attachments/${attachmentId}`);
    } catch (error: any) {
      throw new Error(`Failed to delete attachment: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  private getContentType(fileName: string): string {
    const ext = fileName.split('.').pop()?.toLowerCase();
    const contentTypes: Record<string, string> = {
      'pdf': 'application/pdf',
      'doc': 'application/msword',
      'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'xls': 'application/vnd.ms-excel',
      'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      'png': 'image/png',
      'jpg': 'image/jpeg',
      'jpeg': 'image/jpeg',
      'gif': 'image/gif',
      'txt': 'text/plain',
      'csv': 'text/csv',
      'json': 'application/json'
    };

    return contentTypes[ext || ''] || 'application/octet-stream';
  }
}

See File Upload Patterns for ChatGPT Apps.

Webhook Integration for Real-Time Updates

Asana's webhook system provides real-time notifications for resource changes including tasks, projects, stories, and other entities. Webhooks require a publicly accessible HTTPS endpoint that receives POST requests containing event payloads and responds with 200 status codes within 10 seconds.

Webhook setup involves creating a webhook resource targeting a specific Asana resource (task, project, workspace) and providing your callback URL. Asana performs an initial handshake by sending a secret token in an X-Hook-Secret header, which your endpoint must echo back in the X-Hook-Secret response header to confirm ownership.

Event filtering happens client-side by inspecting the webhook payload's events array. Each event contains resource, parent, user, created_at, type, and action fields. Common actions include added, removed, changed, deleted, and undeleted. Your webhook handler should process relevant events and ignore others.

Webhook reliability requires implementing retry logic for failed event processing, deduplication for repeated events (using event IDs), and signature verification (using HMAC-SHA256 with the webhook secret). For high-volume webhooks, consider queuing events for asynchronous processing rather than blocking the webhook response.

6. Webhook Event Processor (TypeScript)

import crypto from 'crypto';
import express, { Request, Response } from 'express';

interface WebhookEvent {
  action: 'added' | 'removed' | 'changed' | 'deleted' | 'undeleted';
  created_at: string;
  parent: { gid: string; resource_type: string } | null;
  resource: { gid: string; resource_type: string };
  type: 'task' | 'project' | 'story' | 'attachment';
  user: { gid: string; name: string };
}

interface WebhookPayload {
  events: WebhookEvent[];
}

interface AsanaWebhook {
  gid: string;
  resource: { gid: string; resource_type: string };
  target: string;
  active: boolean;
  last_success_at: string | null;
  last_failure_at: string | null;
  created_at: string;
}

export class AsanaWebhookProcessor {
  private webhookSecrets = new Map<string, string>();

  constructor(private client: AxiosInstance) {}

  public async createWebhook(resourceId: string, targetUrl: string, filters?: string[]): Promise<AsanaWebhook> {
    try {
      const data: any = {
        resource: resourceId,
        target: targetUrl
      };

      if (filters) data.filters = filters;

      const response = await this.client.post('/webhooks', { data });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create webhook: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getWebhooks(workspaceId: string, resourceId?: string): Promise<AsanaWebhook[]> {
    try {
      const params: any = { workspace: workspaceId };
      if (resourceId) params.resource = resourceId;

      const response = await this.client.get('/webhooks', { params });
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get webhooks: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async deleteWebhook(webhookId: string): Promise<void> {
    try {
      await this.client.delete(`/webhooks/${webhookId}`);
      this.webhookSecrets.delete(webhookId);
    } catch (error: any) {
      throw new Error(`Failed to delete webhook: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public setupWebhookEndpoint(app: express.Application, path: string): void {
    app.post(path, (req: Request, res: Response) => {
      // Handshake: Asana sends X-Hook-Secret header on webhook creation
      const hookSecret = req.headers['x-hook-secret'];
      if (hookSecret && typeof hookSecret === 'string') {
        console.log('Webhook handshake received');
        res.setHeader('X-Hook-Secret', hookSecret);
        res.status(200).send();
        return;
      }

      // Verify signature (if configured)
      const signature = req.headers['x-hook-signature'];
      if (signature && typeof signature === 'string') {
        const isValid = this.verifyWebhookSignature(req.body, signature);
        if (!isValid) {
          console.error('Invalid webhook signature');
          res.status(401).send('Invalid signature');
          return;
        }
      }

      // Process webhook payload
      const payload: WebhookPayload = req.body;

      if (!payload.events || !Array.isArray(payload.events)) {
        res.status(400).send('Invalid payload');
        return;
      }

      // Process events asynchronously
      this.processEvents(payload.events).catch(error => {
        console.error('Error processing webhook events:', error);
      });

      // Respond immediately to acknowledge receipt
      res.status(200).send('OK');
    });
  }

  private async processEvents(events: WebhookEvent[]): Promise<void> {
    for (const event of events) {
      console.log(`Processing ${event.type} ${event.action} event:`, event.resource.gid);

      switch (event.type) {
        case 'task':
          await this.handleTaskEvent(event);
          break;
        case 'project':
          await this.handleProjectEvent(event);
          break;
        case 'story':
          await this.handleStoryEvent(event);
          break;
        default:
          console.log('Unhandled event type:', event.type);
      }
    }
  }

  private async handleTaskEvent(event: WebhookEvent): Promise<void> {
    // Implement task event handling logic
    // Examples: sync to database, trigger notifications, update dashboards
    console.log(`Task ${event.action}:`, event.resource.gid);
  }

  private async handleProjectEvent(event: WebhookEvent): Promise<void> {
    console.log(`Project ${event.action}:`, event.resource.gid);
  }

  private async handleStoryEvent(event: WebhookEvent): Promise<void> {
    console.log(`Story ${event.action}:`, event.resource.gid);
  }

  private verifyWebhookSignature(payload: any, signature: string): boolean {
    // Implement HMAC-SHA256 signature verification
    // Note: Asana uses webhook secret for signature generation
    // This is a placeholder - actual implementation depends on secret storage
    return true;
  }
}

For webhook patterns, see Real-Time Data Sync with Webhooks.

7. Dependency Manager (TypeScript)

interface TaskDependency {
  gid: string;
  resource_type: 'task';
  name: string;
}

export class AsanaDependencyManager {
  constructor(private client: AxiosInstance) {}

  public async addDependencies(taskId: string, dependsOnTaskIds: string[]): Promise<void> {
    try {
      await this.client.post(`/tasks/${taskId}/addDependencies`, {
        data: { dependencies: dependsOnTaskIds }
      });
    } catch (error: any) {
      throw new Error(`Failed to add dependencies: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async removeDependencies(taskId: string, dependsOnTaskIds: string[]): Promise<void> {
    try {
      await this.client.post(`/tasks/${taskId}/removeDependencies`, {
        data: { dependencies: dependsOnTaskIds }
      });
    } catch (error: any) {
      throw new Error(`Failed to remove dependencies: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getDependencies(taskId: string): Promise<TaskDependency[]> {
    try {
      const response = await this.client.get(`/tasks/${taskId}/dependencies`);
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get dependencies: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getDependents(taskId: string): Promise<TaskDependency[]> {
    try {
      const response = await this.client.get(`/tasks/${taskId}/dependents`);
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get dependents: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async findCircularDependencies(taskId: string, visited: Set<string> = new Set()): Promise<string[]> {
    if (visited.has(taskId)) {
      return [taskId]; // Circular dependency detected
    }

    visited.add(taskId);
    const dependencies = await this.getDependencies(taskId);

    for (const dep of dependencies) {
      const circular = await this.findCircularDependencies(dep.gid, new Set(visited));
      if (circular.length > 0) {
        return [taskId, ...circular];
      }
    }

    return [];
  }
}

Portfolio and Reporting Analytics

Asana portfolios aggregate multiple projects for high-level tracking across initiatives. Portfolios support custom field rollups, status updates, progress tracking, and milestone management. The API provides portfolio queries filtered by workspace, owner, or archived status.

Portfolio status updates enable communicating project health with status colors (green, yellow, red), text updates, and timestamps. These updates appear in portfolio views and provide historical tracking of project trajectory over time.

Progress tracking uses task completion percentages, milestone achievement, and custom field aggregations. The API returns portfolio statistics including total tasks, completed tasks, and custom field summaries for numeric fields (sum, average, etc.).

Reporting analytics require aggregating data across projects and tasks. Common patterns include querying all tasks for a workspace filtered by date ranges, assignees, or custom field values, then computing metrics like completion rates, average time to completion, assignee workload, and project velocity.

8. Portfolio Tracker (TypeScript)

interface PortfolioCreateParams {
  workspace: string;
  name: string;
  color?: string;
  members?: string[];
  public?: boolean;
}

interface AsanaPortfolio {
  gid: string;
  name: string;
  color: string;
  workspace: { gid: string; name: string };
  owner: { gid: string; name: string };
  created_at: string;
  permalink_url: string;
}

interface PortfolioStatusUpdate {
  gid: string;
  status_type: 'on_track' | 'at_risk' | 'off_track' | 'on_hold' | 'complete';
  text: string;
  color: 'green' | 'yellow' | 'red' | 'blue';
  created_at: string;
  author: { gid: string; name: string };
}

export class AsanaPortfolioTracker {
  constructor(private client: AxiosInstance) {}

  public async createPortfolio(params: PortfolioCreateParams): Promise<AsanaPortfolio> {
    try {
      const response = await this.client.post('/portfolios', { data: params });
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create portfolio: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async addProjectToPortfolio(portfolioId: string, projectId: string, insertBefore?: string): Promise<void> {
    try {
      const data: any = { project: projectId };
      if (insertBefore) data.insert_before = insertBefore;

      await this.client.post(`/portfolios/${portfolioId}/addItem`, { data });
    } catch (error: any) {
      throw new Error(`Failed to add project: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getPortfolioProjects(portfolioId: string): Promise<any[]> {
    try {
      const response = await this.client.get(`/portfolios/${portfolioId}/items`);
      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to get portfolio projects: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async createStatusUpdate(
    portfolioId: string,
    statusType: PortfolioStatusUpdate['status_type'],
    text: string
  ): Promise<PortfolioStatusUpdate> {
    try {
      const response = await this.client.post(`/portfolios/${portfolioId}/status_updates`, {
        data: { status_type: statusType, text }
      });

      return response.data.data;
    } catch (error: any) {
      throw new Error(`Failed to create status update: ${error.response?.data?.errors?.[0]?.message || error.message}`);
    }
  }

  public async getPortfolioStats(portfolioId: string): Promise<{
    totalProjects: number;
    onTrack: number;
    atRisk: number;
    offTrack: number;
  }> {
    const projects = await this.getPortfolioProjects(portfolioId);

    const stats = {
      totalProjects: projects.length,
      onTrack: 0,
      atRisk: 0,
      offTrack: 0
    };

    for (const project of projects) {
      const statusColor = project.status_update?.status_type || 'on_track';
      if (statusColor === 'on_track') stats.onTrack++;
      else if (statusColor === 'at_risk') stats.atRisk++;
      else if (statusColor === 'off_track') stats.offTrack++;
    }

    return stats;
  }
}

9. Sync Engine (TypeScript)

interface SyncState {
  lastSyncTime: Date;
  syncedTasks: Map<string, Date>;
  syncedProjects: Map<string, Date>;
}

export class AsanaSyncEngine {
  private syncState: SyncState;

  constructor(private client: AxiosInstance) {
    this.syncState = {
      lastSyncTime: new Date(0),
      syncedTasks: new Map(),
      syncedProjects: new Map()
    };
  }

  public async syncWorkspace(workspaceId: string): Promise<void> {
    console.log('Starting workspace sync...');

    const sinceDate = this.syncState.lastSyncTime.toISOString();

    // Sync projects
    const projects = await this.getModifiedProjects(workspaceId, sinceDate);
    console.log(`Found ${projects.length} modified projects`);

    for (const project of projects) {
      await this.syncProject(project.gid);
    }

    // Update sync time
    this.syncState.lastSyncTime = new Date();
  }

  private async getModifiedProjects(workspaceId: string, modifiedSince: string): Promise<any[]> {
    try {
      const response = await this.client.get(`/workspaces/${workspaceId}/projects`, {
        params: { modified_since: modifiedSince }
      });

      return response.data.data;
    } catch (error: any) {
      console.error('Failed to get modified projects:', error.message);
      return [];
    }
  }

  private async syncProject(projectId: string): Promise<void> {
    try {
      const tasks = await this.client.get(`/projects/${projectId}/tasks`);

      for (const task of tasks.data.data) {
        this.syncState.syncedTasks.set(task.gid, new Date(task.modified_at));
      }

      this.syncState.syncedProjects.set(projectId, new Date());
    } catch (error: any) {
      console.error(`Failed to sync project ${projectId}:`, error.message);
    }
  }

  public getSyncStats(): { totalTasks: number; totalProjects: number; lastSync: Date } {
    return {
      totalTasks: this.syncState.syncedTasks.size,
      totalProjects: this.syncState.syncedProjects.size,
      lastSync: this.syncState.lastSyncTime
    };
  }
}

10. Analytics Reporter (TypeScript)

interface TaskAnalytics {
  totalTasks: number;
  completedTasks: number;
  overdueTasks: number;
  completionRate: number;
  averageCompletionTime: number;
  tasksByAssignee: Map<string, number>;
  tasksByProject: Map<string, number>;
}

export class AsanaAnalyticsReporter {
  constructor(private client: AxiosInstance) {}

  public async generateWorkspaceReport(workspaceId: string, startDate?: Date, endDate?: Date): Promise<TaskAnalytics> {
    const tasks = await this.getTasksForPeriod(workspaceId, startDate, endDate);

    const analytics: TaskAnalytics = {
      totalTasks: tasks.length,
      completedTasks: 0,
      overdueTasks: 0,
      completionRate: 0,
      averageCompletionTime: 0,
      tasksByAssignee: new Map(),
      tasksByProject: new Map()
    };

    let totalCompletionTime = 0;
    const now = new Date();

    for (const task of tasks) {
      if (task.completed) {
        analytics.completedTasks++;

        // Calculate completion time
        const created = new Date(task.created_at);
        const completed = new Date(task.completed_at);
        totalCompletionTime += completed.getTime() - created.getTime();
      }

      // Check overdue
      if (task.due_on && !task.completed) {
        const dueDate = new Date(task.due_on);
        if (dueDate < now) analytics.overdueTasks++;
      }

      // Count by assignee
      if (task.assignee) {
        const count = analytics.tasksByAssignee.get(task.assignee.gid) || 0;
        analytics.tasksByAssignee.set(task.assignee.gid, count + 1);
      }

      // Count by project
      for (const project of task.projects || []) {
        const count = analytics.tasksByProject.get(project.gid) || 0;
        analytics.tasksByProject.set(project.gid, count + 1);
      }
    }

    analytics.completionRate = (analytics.completedTasks / analytics.totalTasks) * 100;
    analytics.averageCompletionTime = analytics.completedTasks > 0
      ? totalCompletionTime / analytics.completedTasks / (1000 * 60 * 60 * 24) // Days
      : 0;

    return analytics;
  }

  private async getTasksForPeriod(workspaceId: string, startDate?: Date, endDate?: Date): Promise<any[]> {
    const params: any = {
      opt_fields: 'name,completed,completed_at,created_at,due_on,assignee.gid,projects.gid'
    };

    if (startDate) params.modified_since = startDate.toISOString();

    const response = await this.client.get(`/workspaces/${workspaceId}/tasks/search`, { params });

    let tasks = response.data.data;

    if (endDate) {
      tasks = tasks.filter((t: any) => new Date(t.created_at) <= endDate);
    }

    return tasks;
  }
}

For analytics patterns, see Building Analytics Dashboards for ChatGPT Apps.

Conclusion: Build Production-Ready Asana Integrations

Asana's comprehensive REST API enables building sophisticated task management integrations for ChatGPT apps. The 10 TypeScript implementations in this guide provide production-ready foundations for OAuth authentication, task and project CRUD operations, custom field management, file attachments, webhook processing, dependency tracking, portfolio analytics, workspace synchronization, and reporting.

Key implementation priorities: implement OAuth 2.0 with automatic token refresh, handle rate limiting with exponential backoff and retry logic, use webhook subscriptions for real-time updates instead of polling, implement custom field mappings for industry-specific metadata, and cache frequently accessed data like workspace configurations and project structures.

For production deployments, consider implementing request queuing to respect rate limits, database persistence for sync state and webhook event deduplication, background job processing for bulk operations, and comprehensive error logging with Sentry or similar monitoring tools.

Ready to build your Asana-powered ChatGPT app? Start with MakeAIHQ's no-code platform and deploy to the ChatGPT App Store in 48 hours. Explore our AI-powered app builder for rapid prototyping, or browse industry-specific templates for fitness studios, restaurants, and professional services.


External Resources

Internal Resources

  • API Integration Best Practices for ChatGPT Apps
  • REST API Best Practices for ChatGPT Apps
  • Building Multi-Tool ChatGPT Apps
  • Real-Time Data Sync with Webhooks
  • File Upload Patterns for ChatGPT Apps
  • Analytics Dashboards for ChatGPT Apps
  • Deploying ChatGPT Apps to Production
  • MakeAIHQ ChatGPT App Builder
  • AI-Powered App Editor
  • Industry Templates