To integrate Replit with Asana, generate a Personal Access Token from your Asana account, store it in Replit Secrets (lock icon π), and use the Asana REST API from your Python or Node.js server to create tasks, manage projects, and subscribe to webhook events. Use Autoscale deployment for web-app-driven project management workflows.
Why Connect Replit to Asana?
Asana is one of the most popular project management tools for teams building software, running marketing campaigns, and coordinating operations. Connecting your Replit app to Asana lets you automate task creation based on app events, sync work items between your app and your team's project board, and build custom reporting dashboards that surface Asana data in your own interface.
The Asana REST API v1 follows standard REST conventions and is well-documented. Every Asana object (task, project, section, user, workspace) has a globally unique identifier called a GID β a numeric string you use in all API calls to reference specific objects. Authentication with a Personal Access Token is straightforward: include it as a Bearer token in the Authorization header. For production apps where multiple users connect their Asana accounts, Asana also supports OAuth 2.0.
Common automation patterns include creating a task in Asana when a support ticket is submitted in your app, marking a task complete when a deployment succeeds, or pushing task status back into your app's database when team members update work in Asana. Replit's Secrets system (lock icon π) keeps your Personal Access Token encrypted and out of your code, and Replit's deployment options give you the stable HTTPS URL needed to receive Asana webhook events.
Integration method
You connect Replit to Asana by generating a Personal Access Token (PAT) from your Asana developer settings, storing it in Replit Secrets, and calling the Asana REST API from your Python or Node.js backend code. The PAT authenticates all requests via the Authorization header and grants access to your Asana workspace without requiring an OAuth flow. For apps used by multiple Asana users, Asana also supports OAuth 2.0.
Prerequisites
- A Replit account with a Python or Node.js project created
- An Asana account with at least one workspace and one project
- Access to Asana developer settings at https://app.asana.com/0/my-apps
- Basic familiarity with REST APIs and HTTP authentication
- Node.js 18+ or Python 3.10+ (both available on Replit by default)
Step-by-step guide
Generate an Asana Personal Access Token and Find Your GIDs
Generate an Asana Personal Access Token and Find Your GIDs
Go to https://app.asana.com/0/my-apps and click 'Create new token'. Give it a descriptive name like 'replit-integration' and click 'Create token'. Copy the token immediately β Asana only shows it once. This Personal Access Token authenticates all API requests you make from Replit. You will also need the GIDs (globally unique identifiers) for your workspace and target projects. To find your workspace GID, call the Asana API directly: make a GET request to https://app.asana.com/api/1.0/workspaces with your token in the Authorization header. The response lists all workspaces with their GIDs. To find a project GID, open the project in Asana in your browser. The URL contains the project GID: https://app.asana.com/0/{workspace_gid}/{project_gid}/list. Copy the numeric string that appears in the position of {project_gid}. For task section GIDs, use the API endpoint GET /projects/{project_gid}/sections to list all sections in a project and their GIDs. Store any GIDs you plan to use frequently as Replit Secrets rather than hardcoding them in your application code β this makes it easy to change target projects without redeploying.
1# Quick helper to find your workspace and project GIDs2import requests3import os45# Temporarily use the token directly to discover GIDs6# Then store everything in Replit Secrets7TOKEN = "your-personal-access-token-here" # Replace, then move to Secrets89headers = {10 "Authorization": f"Bearer {TOKEN}",11 "Accept": "application/json"12}1314# Get workspaces15workspaces_resp = requests.get("https://app.asana.com/api/1.0/workspaces", headers=headers)16for ws in workspaces_resp.json().get("data", []):17 print(f"Workspace: {ws['name']} β GID: {ws['gid']}")1819# Get projects in a workspace (replace WORKSPACE_GID)20WORKSPACE_GID = "your-workspace-gid"21projects_resp = requests.get(22 f"https://app.asana.com/api/1.0/workspaces/{WORKSPACE_GID}/projects",23 headers=headers24)25for project in projects_resp.json().get("data", []):26 print(f"Project: {project['name']} β GID: {project['gid']}")Pro tip: Run this discovery script once, note the GIDs, then delete the hardcoded token and store everything in Replit Secrets. Never commit a token to Git even temporarily.
Expected result: The script prints your workspace name and GID, plus a list of all your Asana projects with their GIDs.
Store Asana Credentials in Replit Secrets
Store Asana Credentials in Replit Secrets
Open your Replit project and click the lock icon π in the left sidebar to open the Secrets pane. Add the following secrets: - Key: ASANA_ACCESS_TOKEN β Value: your Personal Access Token from Asana My Apps - Key: ASANA_WORKSPACE_GID β Value: your workspace GID (found in Step 1) - Key: ASANA_PROJECT_GID β Value: the GID of your main project (found in Step 1) If you plan to create tasks in multiple projects, add separate secrets for each one: ASANA_TASKS_PROJECT_GID, ASANA_BUGS_PROJECT_GID, etc. This makes it easy to target the right project without hardcoding GIDs in your code. Click 'Add Secret' after entering each key-value pair. Replit encrypts these values with AES-256 encryption at rest. They are injected as environment variables at runtime and never appear in your file tree or Git history. In Python, access them with os.environ['ASANA_ACCESS_TOKEN']. In Node.js, use process.env.ASANA_ACCESS_TOKEN. Remember: Replit's Secret Scanner will warn you if it detects API tokens pasted directly into code files. Always use the Secrets pane for any credentials β not .env files, not comments, not variable declarations at the top of your scripts.
Pro tip: After adding Secrets, you must redeploy your app (click 'Deploy' in the toolbar) for the production environment to pick up the new values. Local development picks up Secrets immediately on the next Run.
Expected result: ASANA_ACCESS_TOKEN, ASANA_WORKSPACE_GID, and ASANA_PROJECT_GID appear in the Secrets pane with their values hidden.
Create and Manage Tasks with Python
Create and Manage Tasks with Python
The Asana REST API uses standard HTTP methods with JSON bodies. All requests require the Authorization header with your Bearer token. Responses wrap the main object in a 'data' key. For operations that return multiple objects (like listing tasks in a project), results are paginated using an 'offset' cursor. The code below demonstrates the core task management operations: creating a task in a project, updating a task's fields, marking it complete, adding a comment (called a 'story' in Asana), and listing all incomplete tasks in a project. All API calls use the base URL https://app.asana.com/api/1.0/. Task creation requires at minimum a name and workspace GID. Optionally include notes (description), assignee (user GID), due_on (date string in YYYY-MM-DD format), and projects (array of project GIDs to add the task to). Tasks can exist in multiple projects simultaneously β this is an Asana-specific concept not found in most project management tools.
1import os2import requests3from typing import Optional, List45ASANA_TOKEN = os.environ["ASANA_ACCESS_TOKEN"]6WORKSPACE_GID = os.environ["ASANA_WORKSPACE_GID"]7PROJECT_GID = os.environ["ASANA_PROJECT_GID"]8BASE_URL = "https://app.asana.com/api/1.0"910HEADERS = {11 "Authorization": f"Bearer {ASANA_TOKEN}",12 "Content-Type": "application/json",13 "Accept": "application/json"14}1516def create_task(17 name: str,18 notes: str = "",19 assignee_gid: Optional[str] = None,20 due_on: Optional[str] = None,21 section_gid: Optional[str] = None22) -> dict:23 """Create a task in the configured project."""24 task_data = {25 "data": {26 "name": name,27 "notes": notes,28 "workspace": WORKSPACE_GID,29 "projects": [PROJECT_GID]30 }31 }32 if assignee_gid:33 task_data["data"]["assignee"] = assignee_gid34 if due_on:35 task_data["data"]["due_on"] = due_on # Format: YYYY-MM-DD3637 response = requests.post(f"{BASE_URL}/tasks", json=task_data, headers=HEADERS)38 response.raise_for_status()39 task = response.json()["data"]4041 # Optionally move task to a specific section42 if section_gid:43 move_task_to_section(task["gid"], section_gid)4445 print(f"Created task '{name}' with GID: {task['gid']}")46 return task4748def complete_task(task_gid: str) -> dict:49 """Mark a task as complete."""50 response = requests.put(51 f"{BASE_URL}/tasks/{task_gid}",52 json={"data": {"completed": True}},53 headers=HEADERS54 )55 response.raise_for_status()56 return response.json()["data"]5758def update_task(task_gid: str, updates: dict) -> dict:59 """Update any task fields."""60 response = requests.put(61 f"{BASE_URL}/tasks/{task_gid}",62 json={"data": updates},63 headers=HEADERS64 )65 response.raise_for_status()66 return response.json()["data"]6768def add_comment(task_gid: str, text: str) -> dict:69 """Add a comment (story) to a task."""70 response = requests.post(71 f"{BASE_URL}/tasks/{task_gid}/stories",72 json={"data": {"text": text}},73 headers=HEADERS74 )75 response.raise_for_status()76 return response.json()["data"]7778def list_incomplete_tasks() -> List[dict]:79 """List all incomplete tasks in the configured project."""80 tasks = []81 params = {82 "project": PROJECT_GID,83 "completed_since": "now", # Only incomplete tasks84 "opt_fields": "name,assignee.name,due_on,notes,completed"85 }86 response = requests.get(f"{BASE_URL}/tasks", params=params, headers=HEADERS)87 response.raise_for_status()88 return response.json()["data"]8990def move_task_to_section(task_gid: str, section_gid: str) -> None:91 """Move a task into a specific section."""92 requests.post(93 f"{BASE_URL}/sections/{section_gid}/addTask",94 json={"data": {"task": task_gid}},95 headers=HEADERS96 ).raise_for_status()9798if __name__ == "__main__":99 task = create_task(100 name="Review pull request #42",101 notes="Check the API integration changes in PR #42 before merging.",102 due_on="2026-04-07"103 )104 print(f"Task URL: https://app.asana.com/0/{PROJECT_GID}/{task['gid']}")105106 tasks = list_incomplete_tasks()107 print(f"Incomplete tasks: {len(tasks)}")108 for t in tasks:109 print(f" - {t['name']} (due: {t.get('due_on', 'no date')})")Pro tip: Use opt_fields in your API calls to specify exactly which fields you need in the response. Without opt_fields, Asana returns only the GID and name, which reduces response size and speeds up your app.
Expected result: Running the script creates a task in your Asana project, prints the task GID and URL, and lists all incomplete tasks.
Build an Express Server with Asana Webhook Support
Build an Express Server with Asana Webhook Support
For Node.js projects, or to expose Asana operations as a web API, use Express. The code below creates a server with endpoints for creating tasks and handling Asana webhook events. It also includes the official asana npm package which provides a typed client. Asana webhooks work differently from most webhook systems. When you register a webhook, Asana immediately sends a handshake request with an X-Hook-Secret header. Your server must respond with the same value in the response header β this one-time handshake confirms your endpoint is live. Subsequent webhook deliveries use the X-Hook-Signature header for HMAC verification using the same secret. Webhooks can be registered on any Asana resource: a workspace, project, or task. Project-level webhooks notify you of task events (created, updated, completed, deleted) within that project. This makes them ideal for keeping your app's database in sync with Asana.
1const express = require('express');2const asana = require('asana');3const crypto = require('crypto');45const app = express();6app.use(express.json());78// Initialize Asana client from Replit Secrets9const client = asana.ApiClient.instance;10const token = client.authentications['token'];11token.accessToken = process.env.ASANA_ACCESS_TOKEN;1213const tasksApi = new asana.TasksApi();14const storiesApi = new asana.StoriesApi();15const webhooksApi = new asana.WebhooksApi();1617const PROJECT_GID = process.env.ASANA_PROJECT_GID;18const WORKSPACE_GID = process.env.ASANA_WORKSPACE_GID;1920// Store webhook secret after handshake21let webhookSecret = '';2223// Create a task24app.post('/tasks', async (req, res) => {25 const { name, notes, dueOn, assigneeGid } = req.body;26 if (!name) return res.status(400).json({ error: 'name is required' });2728 try {29 const taskData = {30 data: {31 name,32 notes: notes || '',33 projects: [PROJECT_GID],34 workspace: WORKSPACE_GID,35 due_on: dueOn || undefined,36 assignee: assigneeGid || undefined37 }38 };39 const response = await tasksApi.createTask(taskData);40 const task = response.data;41 res.json({ success: true, gid: task.gid, name: task.name });42 } catch (err) {43 console.error('Create task error:', err.response?.body);44 res.status(500).json({ error: err.message });45 }46});4748// Complete a task49app.patch('/tasks/:gid/complete', async (req, res) => {50 try {51 const response = await tasksApi.updateTask({ data: { completed: true } }, req.params.gid);52 res.json({ success: true, completed: response.data.completed });53 } catch (err) {54 res.status(500).json({ error: err.message });55 }56});5758// Asana webhook handler β handles both handshake and events59app.post('/asana/webhook', (req, res) => {60 // Step 1: One-time handshake when webhook is first registered61 const hookSecret = req.headers['x-hook-secret'];62 if (hookSecret) {63 webhookSecret = hookSecret;64 console.log('Asana webhook handshake received β secret stored');65 res.set('X-Hook-Secret', hookSecret);66 return res.status(200).send();67 }6869 // Step 2: Verify signature on subsequent events70 const signature = req.headers['x-hook-signature'];71 if (webhookSecret && signature) {72 const hmac = crypto.createHmac('sha256', webhookSecret);73 hmac.update(JSON.stringify(req.body));74 const expectedSig = hmac.digest('hex');75 if (signature !== expectedSig) {76 console.warn('Invalid webhook signature');77 return res.status(403).json({ error: 'Invalid signature' });78 }79 }8081 // Process events82 const events = req.body.events || [];83 events.forEach(event => {84 console.log(`Asana event: ${event.action} on ${event.resource?.resource_type} ${event.resource?.gid}`);85 if (event.action === 'changed' && event.resource?.resource_type === 'task') {86 // Handle task change β update your database, send notifications, etc.87 }88 });8990 res.json({ received: true });91});9293// Register a webhook (call once after deploying)94app.post('/register-webhook', async (req, res) => {95 const callbackUrl = `https://${req.headers.host}/asana/webhook`;96 try {97 const webhook = await webhooksApi.createWebhook({98 data: {99 resource: PROJECT_GID,100 target: callbackUrl101 }102 });103 res.json({ success: true, webhookGid: webhook.data.gid });104 } catch (err) {105 res.status(500).json({ error: err.message });106 }107});108109app.listen(3000, '0.0.0.0', () => {110 console.log('Asana integration server running on port 3000');111});Pro tip: Register webhooks only once per deployment URL β calling /register-webhook multiple times creates duplicate webhooks that each send the same events. List your existing webhooks at GET /webhooks?workspace={gid} before registering a new one.
Expected result: The Express server starts, POST /tasks creates Asana tasks, and POST /asana/webhook handles the Asana handshake and logs incoming task events.
Deploy and Set Up Production Webhooks
Deploy and Set Up Production Webhooks
Asana webhooks require a deployed URL to work reliably β development Replit URLs are temporary and go offline when you close the browser tab, causing webhook deliveries to fail silently. Deploy your app to get a stable URL. In Replit, click 'Deploy' and choose Autoscale deployment. Autoscale is appropriate for apps that handle task creation triggered by user actions or other API calls. Autoscale cold starts are typically under 2 seconds, and Asana retries failed webhook deliveries for up to 24 hours, so cold starts are not a problem. If you are running a continuous background process that polls Asana on a schedule, use Reserved VM instead. After deploying, your app is available at https://your-app.replit.app. Call the /register-webhook endpoint (or use the Asana API directly) to register your webhook with Asana. The handshake happens immediately β Asana sends a request and your server must respond with the X-Hook-Secret header within 10 seconds. If deployment is successful and your /asana/webhook endpoint is reachable, the handshake completes automatically. To verify your webhook is active, go to your Asana project, create or update a task, and check your Replit deployment logs for the incoming event. You can also list your active webhooks via GET https://app.asana.com/api/1.0/webhooks?workspace={WORKSPACE_GID} to confirm registration.
Pro tip: Asana sends webhook events in batches and may deliver multiple events in a single POST. Always iterate over req.body.events (or request.json.get('events', [])) rather than assuming a single event per request.
Expected result: Your app is deployed at a stable replit.app URL, the webhook handshake succeeds, and task events in Asana appear in your deployment logs within a few seconds.
Common use cases
Auto-Create Tasks from Form Submissions
When a user submits a contact form, bug report, or feature request in your Replit web app, automatically create an Asana task in the appropriate project and assign it to the right team member. This eliminates manual task creation and ensures nothing falls through the cracks.
Build a Flask endpoint that receives a form submission (name, email, description) and creates an Asana task in a specified project using the Asana API, with the form data as the task description and the ASANA_ACCESS_TOKEN from Replit Secrets.
Copy this prompt to try it in Replit
Deployment Tracking Integration
When a deployment completes successfully (or fails), automatically update an Asana task status to reflect the deployment outcome. A CI/CD webhook calls your Replit endpoint, which marks the 'Deploy to production' task complete or adds a comment with the failure details.
Create an Express webhook endpoint that receives deployment events and updates a specific Asana task using its GID β marking it complete on success or adding a failure comment using the Asana REST API.
Copy this prompt to try it in Replit
Real-Time Project Status Dashboard
Build a custom dashboard in Replit that pulls tasks from multiple Asana projects, groups them by assignee and completion status, and displays team workload. This provides a consolidated view that is not available in Asana's default interface without a Business or Enterprise subscription.
Write a Python Flask API that fetches all incomplete tasks from an Asana project, groups them by assignee name, and returns a JSON summary showing how many tasks each team member has assigned.
Copy this prompt to try it in Replit
Troubleshooting
API returns 401 Unauthorized even though the token looks correct
Cause: The Personal Access Token may have been revoked, or it was not stored correctly in Replit Secrets. Asana tokens are also workspace-scoped β a token from one workspace cannot access tasks in another.
Solution: Go to https://app.asana.com/0/my-apps and verify the token is still listed and active. If it was revoked, create a new one. Check that ASANA_ACCESS_TOKEN in Replit Secrets contains the full token without leading or trailing whitespace. In Node.js, log process.env.ASANA_ACCESS_TOKEN.length to confirm the value is present.
Webhook handshake fails β Asana reports the endpoint is unreachable
Cause: Webhooks can only be registered against deployed URLs. If you try to register a webhook using a development Replit URL (https://{uuid}.{server}.replit.dev), the URL goes offline when the browser tab is closed and the handshake fails.
Solution: Deploy your app using Autoscale or Reserved VM and use the stable https://your-app.replit.app URL for webhook registration. Make sure your endpoint handles both the initial handshake (responding with X-Hook-Secret header) and subsequent event deliveries.
1// Flask: handle both handshake and events at same endpoint2@app.route('/asana/webhook', methods=['POST'])3def asana_webhook():4 hook_secret = request.headers.get('X-Hook-Secret')5 if hook_secret:6 # Handshake β reflect the secret back7 return '', 200, {'X-Hook-Secret': hook_secret}8 # Regular event β process and return 2009 events = request.json.get('events', [])10 return jsonify({'received': True}), 200Task creation fails with 'project: Not a recognized ID: null'
Cause: The ASANA_PROJECT_GID Secret is not set or contains an invalid value. Asana project GIDs are long numeric strings and must be exact.
Solution: Re-run the find_gids.py script from Step 1 to confirm the correct project GID. Update ASANA_PROJECT_GID in Replit Secrets. GIDs are visible in the Asana URL when you open a project: https://app.asana.com/0/{workspace_gid}/{project_gid}/list.
Tasks are created but do not appear in the expected project section
Cause: Creating a task and adding it to a project does not automatically place it in a specific section (column). Sections must be assigned separately using the addTask endpoint.
Solution: After creating the task, call the sections endpoint to move it to the desired section. Find section GIDs using GET /projects/{project_gid}/sections, then POST to /sections/{section_gid}/addTask with the task GID.
1# Python: move task to section after creation2def move_to_section(task_gid: str, section_gid: str):3 requests.post(4 f"{BASE_URL}/sections/{section_gid}/addTask",5 json={"data": {"task": task_gid}},6 headers=HEADERS7 ).raise_for_status()Best practices
- Store your Asana Personal Access Token in Replit Secrets (lock icon π) β never in source code, .env files checked into Git, or client-side JavaScript.
- Store project and workspace GIDs as separate Replit Secrets rather than hardcoding them β this makes it easy to point the integration at a different project without code changes.
- Use opt_fields in all list and get API calls to specify only the fields you need β this reduces response payload size and speeds up your application.
- Deploy your app before registering Asana webhooks β use your stable replit.app URL, not the temporary development URL.
- Handle the Asana webhook handshake correctly: respond to the initial X-Hook-Secret header by echoing it back in the response header within 10 seconds.
- Iterate over the events array in webhook payloads β Asana batches multiple events into a single POST request, not one event per request.
- Use Autoscale deployment for web apps handling on-demand task creation; use Reserved VM for background processes that continuously sync data from Asana.
- Add error handling for Asana API rate limits (429 responses) β implement exponential backoff when rate-limited, as the API allows 1,500 requests per minute per user.
Alternatives
Trello uses a simpler Kanban board model with API key + token authentication, making it faster to set up for teams who prefer visual card-based workflows over Asana's list and timeline views.
ClickUp includes docs, goals, and time tracking in addition to task management, making it a better choice for teams wanting an all-in-one workspace instead of Asana's focused project management.
Notion combines knowledge base and database features with task tracking, making it preferable for teams that want a single tool for documentation and project management.
Frequently asked questions
How do I store my Asana API token in Replit?
Click the lock icon π in the left sidebar of your Replit project to open the Secrets pane. Add ASANA_ACCESS_TOKEN with your Personal Access Token from https://app.asana.com/0/my-apps. Access it in Python with os.environ['ASANA_ACCESS_TOKEN'] or in Node.js with process.env.ASANA_ACCESS_TOKEN. Never paste the token directly into your code files.
Does Replit work with Asana on the free plan?
Yes. The Asana REST API is available on all Asana plans including free. The free tier allows up to 15 team members and unlimited tasks, which is sufficient for most integrations. Replit's free tier supports outbound API calls without restriction. You will need Replit's paid plan for always-on deployments required to receive Asana webhook events reliably.
What is an Asana GID and how do I find it?
A GID (globally unique identifier) is the numeric string Asana uses to identify every object (workspace, project, task, user). Find project GIDs in the Asana URL when you open a project: https://app.asana.com/0/{workspace_gid}/{project_gid}/list. Find workspace and project GIDs programmatically by calling the /workspaces and /projects API endpoints with your access token.
How do I receive Asana webhook events in my Replit app?
Deploy your Replit app to get a stable URL, then register a webhook using POST https://app.asana.com/api/1.0/webhooks with your project GID and deployed callback URL. Asana immediately sends a handshake request β your server must respond with the X-Hook-Secret header value echoed back. After the handshake, Asana delivers event batches to your endpoint whenever tasks are created, updated, or completed.
What is the Asana API rate limit?
Asana allows 1,500 API requests per minute per user. If you exceed this limit, the API returns 429 responses. Implement exponential backoff: wait 1 second on the first 429, 2 seconds on the second, and so on. For bulk operations, use the batch API endpoint to combine multiple requests into one.
Can I create tasks in multiple Asana projects from Replit?
Yes. In the task creation payload, the 'projects' field accepts an array of project GIDs. A single task can be added to multiple projects simultaneously β this is a native Asana feature. Store each project GID as a separate Replit Secret and pass the appropriate array based on your business logic.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation