Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Asana

Integrate Bolt.new with Asana using a personal access token from the Asana Developer Console and the Asana REST API. Fetch projects, tasks, and portfolios, create tasks from Bolt forms, update task statuses, and build custom work management dashboards — all through a Next.js API route with Bearer token authentication. The official `asana` npm package is pure JavaScript and works in Bolt's WebContainer.

What you'll learn

  • How to generate an Asana personal access token from the Developer Console
  • How to fetch workspaces, projects, and tasks from Asana through a secure server-side API route
  • How to build a custom project dashboard showing tasks by assignee, section, and due date
  • How to create tasks from Bolt forms with assignees, due dates, and custom fields
  • How to handle Asana webhooks for real-time task update notifications after deploying
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate16 min read30 minutesProductivityApril 2026RapidDev Engineering Team
TL;DR

Integrate Bolt.new with Asana using a personal access token from the Asana Developer Console and the Asana REST API. Fetch projects, tasks, and portfolios, create tasks from Bolt forms, update task statuses, and build custom work management dashboards — all through a Next.js API route with Bearer token authentication. The official `asana` npm package is pure JavaScript and works in Bolt's WebContainer.

Building Custom Project Dashboards and Work Automations with Asana and Bolt.new

Asana's REST API provides comprehensive access to workspaces, projects, tasks, sections, portfolios, goals, teams, and users. With this API you can build views and automations that Asana's interface does not natively support — consolidated cross-project task views, automated task creation triggered by form submissions or CRM events, custom reporting dashboards with metrics Asana's built-in analytics do not provide, and read-only project status pages for stakeholders without Asana accounts.

Asana uses two authentication approaches: personal access tokens for single-user or server-side applications, and OAuth 2.0 for apps where multiple users connect their own Asana accounts. Personal access tokens are simpler — generate one from app.asana.com/0/developer-console and use it as a Bearer token in every API request. For most Bolt integrations (personal tools, internal dashboards, automated task creation), personal access tokens are the right choice.

The official `asana` npm package from Asana is written in pure JavaScript and communicates over HTTP, making it fully compatible with Bolt's WebContainer runtime. You can install it with npm and use it directly in your API routes for a cleaner developer experience than raw fetch calls. Alternatively, using fetch directly gives you more control and smaller bundle size — both approaches are shown in this tutorial. Since all Asana API communication uses outbound HTTP requests, the entire integration can be built and tested in Bolt's development environment without deploying first.

Integration method

Bolt Chat + API Route

Bolt generates the Asana integration through conversation — you describe the project dashboard or task automation you want and Bolt writes the API route and React component code. Asana uses Bearer token authentication with personal access tokens, making it straightforward to set up without OAuth flows for single-user apps. All API calls go through server-side Next.js routes to keep the token out of the browser bundle.

Prerequisites

  • An Asana account with at least one workspace containing projects and tasks
  • An Asana personal access token (generate from app.asana.com/0/developer-console → Personal access tokens → New access token)
  • A Next.js project in Bolt.new (prompt 'Create a Next.js app' to get started)
  • Your workspace GID and project GIDs (visible in Asana URLs: app.asana.com/0/{workspaceGid}/{projectGid})

Step-by-step guide

1

Generate your Asana personal access token and configure environment variables

Asana personal access tokens provide full API access on behalf of your user account. To generate one, log into Asana and navigate to app.asana.com/0/developer-console. Click 'Create new token', give it a descriptive name like 'Bolt Integration', and copy the generated token immediately — Asana only shows it once. If you need to see it again, you must generate a new token. Unlike OAuth tokens that expire, personal access tokens persist until you manually revoke them. This makes them ideal for server-side applications that run continuously. The trade-off is that the token represents your personal identity — API actions will be attributed to your Asana account. For apps used by multiple team members, an OAuth flow is more appropriate, but for personal dashboards and automations, a personal token is simpler and sufficient. Asana uses standard Bearer token authentication. Every API request includes an Authorization header in the format 'Bearer {your-token}'. Store the token as ASANA_ACCESS_TOKEN in your .env.local file. Your workspace GID is the number in the Asana URL when viewing your workspace. Project GIDs follow the same pattern — the number in the URL when a project is open. You can discover your workspace GID programmatically by calling GET https://app.asana.com/api/1.0/workspaces with your token — this returns all workspaces your account has access to. Similarly, GET /projects in a workspace returns all visible projects. Both calls work from Bolt's WebContainer during development.

Bolt.new Prompt

Create a Next.js app with an Asana integration. Add a .env.local file with ASANA_ACCESS_TOKEN and ASANA_WORKSPACE_GID as placeholder variables. Create a /api/asana/workspaces route that fetches all workspaces accessible to my token and returns their names and GIDs.

Paste this in Bolt.new chat

.env.local
1// .env.local
2ASANA_ACCESS_TOKEN=your_personal_access_token
3ASANA_WORKSPACE_GID=your_workspace_gid
4ASANA_PROJECT_GID=your_default_project_gid

Pro tip: Asana GIDs are numeric strings (like '1234567890123456'). They appear in Asana URLs and in API responses as the gid field. Always use the string type for GIDs in TypeScript — they can be longer than JavaScript's MAX_SAFE_INTEGER.

Expected result: Your .env.local file is configured, and calling /api/asana/workspaces returns an array of workspace objects with gid and name fields.

2

Fetch projects and tasks from Asana

Asana's API uses a consistent pattern: most endpoints return an array of objects under a 'data' key, each containing at minimum a 'gid' and 'name'. Use the 'opt_fields' query parameter to request specific fields beyond the defaults — this dramatically reduces response size and API latency by avoiding the return of unused data. To fetch projects in a workspace, call GET /projects?workspace={workspaceGid}. To fetch tasks in a project, call GET /tasks?project={projectGid}. The tasks endpoint supports important filter parameters: completed_since ('now' returns only incomplete tasks), due_on.before, due_on.after, assignee, and modified_since for incremental updates. Task objects by default only return gid and name. Use opt_fields to request additional properties: completed, due_on, assignee.name, assignee.gid, memberships.section.name, memberships.project.name, notes, tags.name, custom_fields, and num_subtasks. The memberships field is particularly useful — it tells you which section within the project the task belongs to. Asana paginates results with a default limit of 20 items per page. Use the limit parameter (max 100) to increase this, and check the next_page.offset value in responses to fetch subsequent pages. For projects with hundreds of tasks, implement pagination to ensure you retrieve all tasks. All Asana API calls are outbound HTTP requests that work in Bolt's WebContainer without any deployment. Build your project dashboard in the Bolt preview and see live Asana data during development.

Bolt.new Prompt

Build a /api/asana/tasks route in Next.js that fetches all incomplete tasks from a project GID. Request opt_fields: gid, name, completed, due_on, assignee.name, assignee.gid, memberships.section.name, notes, tags.name. Group the tasks by section name in the response. Also build a React project dashboard component that displays the sections as columns with task cards showing name, due date, and assignee.

Paste this in Bolt.new chat

app/api/asana/tasks/route.ts
1// app/api/asana/tasks/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4const ASANA_API = 'https://app.asana.com/api/1.0';
5const ACCESS_TOKEN = process.env.ASANA_ACCESS_TOKEN;
6const DEFAULT_PROJECT_GID = process.env.ASANA_PROJECT_GID;
7
8type AsanaTask = {
9 gid: string;
10 name: string;
11 completed: boolean;
12 due_on: string | null;
13 assignee: { gid: string; name: string } | null;
14 memberships: Array<{ section: { name: string } }>;
15 notes: string;
16};
17
18async function asanaFetch(path: string, params: Record<string, string> = {}) {
19 const url = new URL(`${ASANA_API}${path}`);
20 Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
21
22 const res = await fetch(url.toString(), {
23 headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
24 });
25
26 if (!res.ok) throw new Error(`Asana API ${res.status}: ${await res.text()}`);
27 return res.json();
28}
29
30export async function GET(request: NextRequest) {
31 const { searchParams } = new URL(request.url);
32 const projectGid = searchParams.get('project') ?? DEFAULT_PROJECT_GID;
33
34 const data = await asanaFetch('/tasks', {
35 project: projectGid!,
36 completed_since: 'now',
37 limit: '100',
38 opt_fields: 'gid,name,completed,due_on,assignee.name,assignee.gid,memberships.section.name,notes,tags.name',
39 });
40
41 const tasks: AsanaTask[] = data.data;
42
43 // Group tasks by section
44 const sections: Record<string, AsanaTask[]> = {};
45 for (const task of tasks) {
46 const sectionName = task.memberships?.[0]?.section?.name ?? 'No Section';
47 if (!sections[sectionName]) sections[sectionName] = [];
48 sections[sectionName].push(task);
49 }
50
51 return NextResponse.json({ sections, total: tasks.length });
52}

Pro tip: Always include opt_fields in your Asana API calls. Without it, Asana returns only gid and name, and you would need separate API calls for each task's details — rapidly consuming your rate limit on larger projects.

Expected result: Calling /api/asana/tasks returns tasks grouped by section with names, due dates, and assignee information. The project dashboard component renders the sections with task cards showing all requested fields.

3

Create and update tasks from Bolt forms

Creating a task in Asana requires a POST to /tasks with a 'data' wrapper object. Required fields vary: at minimum you need either 'projects' (an array of project GIDs to add the task to) or 'workspace' plus 'assignee'. Recommended fields for a useful task: name, notes (the task description, plain text), due_on (ISO 8601 date string like '2026-06-15'), assignee (user GID), projects (array of project GIDs), memberships (array of objects with project and section GIDs to place the task in a specific section), and tags (array of tag GIDs). Note the Asana API's request body format: all fields must be nested under a 'data' key: { data: { name: '...', projects: ['...'] } }. Omitting this wrapper results in a 400 error. Updating a task uses PUT /tasks/{taskGid} with the same 'data' wrapper, including only the fields you want to change. To mark a task complete, PUT /tasks/{taskGid} with { data: { completed: true } }. To add a comment (Asana calls them 'stories'), POST to /tasks/{taskGid}/stories with { data: { text: '...' } }. For custom fields — fields unique to your workspace configuration — you need the custom field GID and the appropriate value format. Retrieve custom field definitions from GET /projects/{projectGid} with opt_fields=custom_fields to see which custom fields exist and their GIDs and types. Task creation and updates are outbound HTTP calls that work in Bolt's WebContainer. You can create real Asana tasks from your Bolt development environment without deploying — a convenient way to verify your form integrations are working before going to production.

Bolt.new Prompt

Add task creation and updates to my Asana integration. Create a /api/asana/tasks/create POST route that creates a task with: name, notes, project_gid, section_gid (optional), assignee_gid (optional), and due_on (optional date string). Remember to wrap the body in { data: {} }. Create a /api/asana/tasks/[gid]/complete POST route that marks a task complete. Add a task creation form that calls this route and shows the new task URL in a success message.

Paste this in Bolt.new chat

app/api/asana/tasks/create/route.ts
1// app/api/asana/tasks/create/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4const ASANA_API = 'https://app.asana.com/api/1.0';
5const ACCESS_TOKEN = process.env.ASANA_ACCESS_TOKEN;
6
7export async function POST(request: NextRequest) {
8 const { name, notes, project_gid, section_gid, assignee_gid, due_on } = await request.json();
9
10 if (!name || !project_gid) {
11 return NextResponse.json(
12 { error: 'name and project_gid are required' },
13 { status: 400 }
14 );
15 }
16
17 // Asana requires the 'data' wrapper in all request bodies
18 const taskData: Record<string, unknown> = {
19 name,
20 projects: [project_gid],
21 };
22
23 if (notes) taskData.notes = notes;
24 if (due_on) taskData.due_on = due_on;
25 if (assignee_gid) taskData.assignee = assignee_gid;
26 if (section_gid) {
27 taskData.memberships = [{ project: project_gid, section: section_gid }];
28 }
29
30 const response = await fetch(`${ASANA_API}/tasks`, {
31 method: 'POST',
32 headers: {
33 Authorization: `Bearer ${ACCESS_TOKEN}`,
34 'Content-Type': 'application/json',
35 },
36 body: JSON.stringify({ data: taskData }), // <-- Required 'data' wrapper
37 });
38
39 if (!response.ok) {
40 const error = await response.json();
41 return NextResponse.json({ error: error.errors }, { status: response.status });
42 }
43
44 const result = await response.json();
45 const task = result.data;
46 return NextResponse.json({
47 gid: task.gid,
48 name: task.name,
49 permalink_url: task.permalink_url,
50 });
51}

Pro tip: Every Asana API write request requires the { data: { ...fields } } wrapper in the request body. This is Asana's consistent envelope format — forgetting it is the single most common mistake when first using the Asana API.

Expected result: The task creation form successfully creates tasks in Asana, and the response includes a permalink_url you can display as a link to the newly created task in the Asana web app.

4

Deploy and set up Asana webhooks for real-time updates

Asana webhooks allow your app to receive real-time push notifications when tasks are created, updated, or deleted — eliminating the need for polling and ensuring your dashboard reflects changes instantly. Asana webhooks use a two-step handshake: when you register a webhook, Asana sends a handshake request containing an X-Hook-Secret header, and your handler must echo back that secret in the response header. After a successful handshake, Asana sends events as POST requests. Before setting up webhooks, deploy your app to Netlify or Vercel. In Bolt, click the Publish button to deploy to Netlify. Your app gets a public URL like your-app.netlify.app. After deployment, add your environment variables (ASANA_ACCESS_TOKEN, ASANA_WORKSPACE_GID, ASANA_PROJECT_GID) in Netlify's Site Settings → Environment Variables. Redeploy after adding variables. Register the webhook by calling POST https://app.asana.com/api/1.0/webhooks with the data wrapper: { data: { resource: projectGid, target: 'https://your-app.netlify.app/api/asana/webhook', filters: [{ resource_type: 'task', action: 'changed' }] } }. Your webhook handler must respond to the initial handshake synchronously within 10 seconds. Each webhook event payload is an array of 'events' with resource (the changed object), resource_type, action, and change details. Multiple changes can be batched into a single webhook call. Process them efficiently and return 200 immediately. During development in Bolt's WebContainer, incoming webhooks cannot reach your app since it runs in the browser with no public URL. This is a fundamental constraint of WebContainers architecture — the browser sandbox cannot accept inbound TCP connections. Use polling (refetch every 30 seconds) as an alternative during development, and switch to webhooks after deployment.

Bolt.new Prompt

Add an Asana webhook handler to my app at /api/asana/webhook. Handle the Asana handshake: if the request contains an X-Hook-Secret header, return it back in the response as X-Hook-Secret with a 200 status. For regular webhook events, parse the events array and log each event's resource_type, action, and resource gid. Add ASANA_WEBHOOK_SECRET to .env.local for verifying incoming events.

Paste this in Bolt.new chat

app/api/asana/webhook/route.ts
1// app/api/asana/webhook/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4export async function POST(request: NextRequest) {
5 // Step 1: Asana handshake — echo back the X-Hook-Secret header
6 const hookSecret = request.headers.get('x-hook-secret');
7 if (hookSecret) {
8 return new NextResponse(null, {
9 status: 200,
10 headers: { 'X-Hook-Secret': hookSecret },
11 });
12 }
13
14 // Step 2: Process regular webhook events
15 const payload = await request.json();
16 const events: Array<{
17 resource: { gid: string; name?: string };
18 resource_type: string;
19 action: string;
20 change?: { field: string; new_value: unknown };
21 }> = payload.events ?? [];
22
23 for (const event of events) {
24 console.log(`Asana event: ${event.resource_type} ${event.action}`, {
25 gid: event.resource.gid,
26 name: event.resource.name,
27 change: event.change,
28 });
29 }
30
31 return NextResponse.json({ received: events.length });
32}

Pro tip: Asana's webhook handshake happens only once during registration. Your handler must respond synchronously with the X-Hook-Secret echoed in the response header — a delayed or incorrect response fails the registration and Asana will not send future events.

Expected result: After deploying and registering the Asana webhook, the handshake completes successfully and your app begins receiving real-time event notifications. Task updates in Asana appear in your server logs within seconds of being made.

Common use cases

Cross-project task dashboard by assignee

Build a consolidated view showing all tasks assigned to a specific team member across multiple Asana projects, with color-coded project labels, due date indicators, and priority sorting. Team leads can see full workload at a glance without opening each project separately.

Bolt.new Prompt

Build an Asana task dashboard in Next.js. Fetch all incomplete tasks assigned to a specific user ID across multiple project GIDs from the Asana API. Display them as cards grouped by project, showing: task name, due date (red if overdue), project name as a color badge, and section name. Add a header with the total task count and a count of tasks due this week.

Copy this prompt to try it in Bolt.new

Automated task creation from form submissions

When users submit a support request, project brief, or content request form in your Bolt app, automatically create an Asana task in the appropriate project and section with the form data pre-filled, assign it to the right team member, and set a due date based on the request type's SLA.

Bolt.new Prompt

Add an Asana integration to my content request form. When a user submits a request with: title, description, content type (blog/video/social), and deadline, create an Asana task in project GID [PROJECT_GID]. Set the task name to the title, notes to the description, due_on to the deadline. Assign to user GID [USER_GID]. Add a tag for the content type. Return the task URL for a confirmation message.

Copy this prompt to try it in Bolt.new

Project status report generator

Build a weekly project status report page that fetches all tasks from an Asana project, calculates completion percentage by section, identifies overdue tasks, and formats a clean summary that can be emailed to stakeholders — saving the project manager 30 minutes of manual reporting each week.

Bolt.new Prompt

Build an Asana project status report page. Fetch all tasks from project GID [PROJECT_GID] grouped by section. For each section, calculate: total tasks, completed tasks, completion percentage, and overdue tasks. Display a progress bar per section. Add a 'Generate Report' button that formats the data as a readable text summary and copies it to clipboard for pasting into email or Slack.

Copy this prompt to try it in Bolt.new

Troubleshooting

All API calls return 401 Unauthorized

Cause: The personal access token is invalid, expired, or not included correctly in the Authorization header. Unlike some APIs, Asana requires the exact format 'Bearer {token}' — missing the 'Bearer' prefix results in a 401.

Solution: Verify your ASANA_ACCESS_TOKEN in .env.local matches the token from app.asana.com/0/developer-console. Confirm the header format is exactly: Authorization: `Bearer ${process.env.ASANA_ACCESS_TOKEN}`. Test the token directly by opening https://app.asana.com/api/1.0/users/me with your browser's fetch or a tool like Postman.

typescript
1// Correct:
2headers: { Authorization: `Bearer ${process.env.ASANA_ACCESS_TOKEN}` }
3
4// Wrong — will return 401:
5headers: { Authorization: process.env.ASANA_ACCESS_TOKEN }

Task creation returns 400 Bad Request with 'Invalid Request'

Cause: The request body is missing the required 'data' wrapper, the project GID does not exist or is not accessible to your token, or a required field is missing. Asana requires all write request bodies to be wrapped in { data: { ...fields } }.

Solution: Ensure your POST body is wrapped in the data envelope: JSON.stringify({ data: { name: '...', projects: ['...'] } }). Verify the project GID by calling GET /api/asana/tasks to see projects accessible to your token. Check that your Content-Type header is set to 'application/json'.

typescript
1// Required format — always wrap in { data: {} }:
2body: JSON.stringify({
3 data: {
4 name: 'Task name',
5 projects: [projectGid],
6 }
7})

Webhook registration fails — Asana shows 'Target URL did not respond correctly to the handshake'

Cause: Your webhook handler is not deployed yet (Bolt's WebContainer has no public URL), the handler did not respond within 10 seconds, or it did not correctly echo back the X-Hook-Secret in the response header (must be a response header, not in the body).

Solution: Deploy your app to Netlify or Vercel before registering the webhook. Verify the /api/asana/webhook route is accessible at your deployed URL. Check that your handler reads the X-Hook-Secret from request headers and returns it as a response header — not in the JSON body.

typescript
1// Correct: secret in response HEADER
2return new NextResponse(null, {
3 status: 200,
4 headers: { 'X-Hook-Secret': hookSecret },
5});
6
7// Wrong: secret in response BODY
8return NextResponse.json({ 'X-Hook-Secret': hookSecret });

Best practices

  • Always use opt_fields in Asana API requests to specify exactly which fields you need — without it, Asana returns minimal data (just gid and name) and you waste API calls fetching details separately.
  • Wrap all Asana write request bodies in the { data: {} } envelope — this is Asana's required format for all POST and PUT requests and the most common source of 400 errors for new integrations.
  • Cache workspace and project data on the server — workspace structure changes rarely and caching reduces unnecessary API calls against Asana's rate limit of 150 requests per minute.
  • Use the Asana 'sync' API endpoint for incremental task updates rather than fetching all tasks on every poll — sync returns only tasks modified since your last sync token, dramatically reducing data transfer for active projects.
  • Store GIDs as string types in TypeScript, not numbers — Asana GIDs can exceed JavaScript's MAX_SAFE_INTEGER and lose precision if stored as numbers.
  • For Asana webhook handlers, return 200 immediately and process events asynchronously — Asana will retry events that do not receive a prompt 200 response, potentially causing duplicate processing.
  • Test your integration with a dedicated Asana sandbox project before connecting to your live project workspace — task creation and updates are real operations that send notifications to team members.
  • Implement idempotent task creation by checking for existing tasks with the same title before creating new ones — form resubmissions or network retries can create duplicate tasks without this check.

Alternatives

Frequently asked questions

How do I connect Bolt.new to Asana?

Generate a personal access token from app.asana.com/0/developer-console and add it as ASANA_ACCESS_TOKEN in your .env.local file. Use Bolt chat to generate Next.js API routes that call the Asana REST API with Bearer token authentication. Use opt_fields in every request to specify which task fields to return. All read and write operations work in Bolt's WebContainer preview without deploying.

Does Bolt.new support Asana webhooks?

Asana webhooks require a publicly accessible URL for both the initial handshake and ongoing event delivery. Bolt's WebContainer runs in the browser and cannot receive inbound HTTP requests. Deploy your app to Netlify or Vercel first, then register the webhook using the Asana API with your deployed endpoint URL.

What is the 'data' wrapper in Asana API requests?

Asana requires all POST and PUT request bodies to be wrapped in a 'data' envelope: { data: { ...your fields } }. For example, to create a task, your body must be JSON.stringify({ data: { name: 'Task name', projects: ['gid'] } }). Omitting this wrapper results in a 400 Bad Request error. This is Asana's consistent request format across all endpoints.

Can I test the Asana integration in Bolt's preview before deploying?

Yes, for all read and write operations. Fetching projects and tasks, creating tasks, updating task fields, marking tasks complete, and adding comments all work in Bolt's WebContainer since they are outbound HTTP requests. Only Asana webhooks require a deployed public URL for their handshake and event delivery.

How do I find my Asana workspace GID and project GIDs?

Workspace GIDs appear in Asana URLs when viewing your workspace, or call GET https://app.asana.com/api/1.0/workspaces with your token. Project GIDs appear in the URL when a project is open in Asana: app.asana.com/0/{workspaceGid}/{projectGid}. You can also call GET /projects?workspace={workspaceGid} to list all projects with their GIDs.

What is Asana's API rate limit?

Asana limits API calls to 150 requests per minute per token on the free plan, with higher limits on paid plans. If you exceed this, the API returns a 429 response with a Retry-After header. For dashboard applications fetching data on page load, this limit is rarely an issue. For bulk operations, use the Asana 'sync' endpoint to fetch only changed data rather than re-fetching all tasks.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.