Connect FlutterFlow to Asana using a FlutterFlow API Call with a Bearer personal access token. Create an API Group pointing at app.asana.com/api/1.0, set the Authorization header at the group level, then chain GET /workspaces and GET /projects to capture GIDs before fetching tasks. Always include the opt_fields query parameter — without it, Asana returns only gid and name, leaving date and assignee bindings silently null.
| Fact | Value |
|---|---|
| Tool | Asana |
| Category | Productivity |
| Method | FlutterFlow API Call |
| Difficulty | Intermediate |
| Time required | 50 minutes |
| Last updated | July 2026 |
Build a Team Task App on Asana Data in FlutterFlow
Asana's REST API v1 gives you full access to your workspace's tasks, projects, teams, and users — making it ideal for building a custom mobile interface on top of your existing Asana data. Whether you're building a client-facing progress tracker, a focused team sprint view, or a personal task companion, the Asana API provides the building blocks.
Asana's REST API is available on all plan tiers including Free. Authentication uses Bearer tokens — you generate a personal access token (PAT) from asana.com/0/my-apps and include it in an Authorization header. PATs are scoped to your individual Asana account and support up to 1,500 requests per minute, which is generous for most mobile apps.
The most important Asana-specific concept to understand before you build: every API response is wrapped in a 'data' envelope. A GET /tasks call doesn't return an array directly — it returns { "data": [...tasks...] }. If you configure JSON Path bindings without the .data prefix (binding $[*].name instead of $.data[*].name), your FlutterFlow ListView will always be empty. Additionally, without the opt_fields query parameter, Asana returns only the gid and name fields — any binding for due_on, assignee, completed, or other fields will silently return null. These two patterns — the data envelope and opt_fields — are the #1 and #2 sources of confusion when connecting FlutterFlow to Asana.
Integration method
FlutterFlow connects to Asana's REST API v1 using Bearer authentication with a personal access token. Every Asana API response is wrapped in a 'data' envelope and objects are identified by opaque GID strings rather than names. You chain GET calls to capture workspace and project GIDs, then fetch tasks using the opt_fields query parameter to request the specific fields you want to display in your FlutterFlow widgets.
Prerequisites
- An Asana account (any plan including Free) with access to the workspace you want to display
- An Asana personal access token generated at asana.com/0/my-apps → New Access Token
- The GID of the workspace or project you want to display (you'll fetch these via API in the tutorial)
- A FlutterFlow project with the API Calls panel available
- A Firebase project if you want to protect write operations (task creation, completion) via Cloud Functions
Step-by-step guide
Generate your Asana personal access token
Navigate to asana.com/0/my-apps in your browser. This is Asana's developer portal where you manage personal access tokens and OAuth apps. Click New Access Token, give it a label like 'FlutterFlow integration', and click Create. Copy the token immediately — Asana only shows it once at creation time. The personal access token (PAT) works for all Asana REST v1 endpoints and is tied to your individual account. It supports up to 1,500 requests per minute for PATs (compared to 150/minute for OAuth apps). This generous limit means most FlutterFlow apps won't hit rate limits unless they fan out across many projects simultaneously. The PAT inherits your account's permissions — it can access every workspace, project, and task you can see in Asana. This means it's a broadly scoped credential. For read-only FlutterFlow features (browsing tasks and projects), storing it in App Constants is reasonable. For write operations like creating or completing tasks, route those through a backend proxy to prevent the token from being extractable from the compiled app. Also note: PATs don't expire by default, but you can revoke them at any time from asana.com/0/my-apps. If you suspect the token has been exposed, revoke it immediately and generate a new one.
Pro tip: Copy the PAT immediately after creating it. Asana only shows it once. If you lose it, you must revoke it and generate a new one.
Expected result: You have your Asana personal access token copied and ready to use in FlutterFlow.
Create the Asana API Group with Bearer auth
In FlutterFlow, click API Calls in the left navigation sidebar. Click + Add → Create API Group. Name the group Asana. Set the Base URL to https://app.asana.com/api/1.0 — this is the base for all Asana REST v1 endpoints. No trailing slash. Add the group-level Authorization header. In the Headers section, click Add Header. Set the key to Authorization and the value to Bearer YOUR_PAT — replace YOUR_PAT with your actual token. Alternatively, go to Settings → App Values (App Constants), create a constant named ASANA_TOKEN with your token value, and set the header to Bearer {{ASANA_TOKEN}} to keep the token in one configurable place. All child API Calls inside this group will inherit this Authorization header automatically. You'll never need to set it on individual calls. Save the group. Now you have a base configuration that all Asana calls will share. The next step is to make your first API call to discover workspace GIDs — Asana's way of identifying objects.
1// API Group configuration reference2// Group name: Asana3// Base URL: https://app.asana.com/api/1.04// Headers (inherited by all child calls):5// Authorization: Bearer {{ASANA_TOKEN}}6// Accept: application/jsonPro tip: Store your PAT as an App Constant named ASANA_TOKEN and reference it as Bearer {{ASANA_TOKEN}} in the header — this makes it easy to rotate the token without hunting through individual call configurations.
Expected result: An Asana API Group is saved in FlutterFlow with the base URL and Bearer Authorization header configured.
Fetch workspaces and projects to capture GIDs
Asana identifies every object — workspaces, projects, tasks, users — by an opaque GID (Global ID) string, not by name. Before you can fetch tasks from a specific project, you need the project's GID. The cleanest approach is to fetch it once via API and store it in App State. Add a new API Call inside the Asana group. Name it GetWorkspaces. Method: GET. Endpoint: /workspaces?opt_fields=name,gid. Test this call — you'll see a response like { "data": [{ "gid": "1234567890", "name": "My Workspace" }] }. Note the .data envelope. Copy the GID of your target workspace from the test response. Next, add another API Call. Name it GetProjects. Method: GET. Endpoint: /projects?workspace={{workspaceGid}}&opt_fields=name,gid. Add a variable named workspaceGid. Test it with your workspace GID — you'll see all projects in that workspace. In your FlutterFlow app, create an App State variable named currentWorkspaceGid (String) and another named currentProjectGid (String). On your home page, add a page-load action that calls GetWorkspaces, then sets currentWorkspaceGid from the first item's GID ($.data[0].gid). If the user might have multiple workspaces, build a picker page. Similarly, set currentProjectGid when the user selects a project. By storing GIDs in App State, you avoid re-fetching parent objects every time the user navigates between pages — a common source of unnecessary API calls.
1// GET /workspaces response structure2{3 "data": [4 {5 "gid": "1234567890123456",6 "name": "My Company Workspace",7 "resource_type": "workspace"8 }9 ]10}11// JSON Path for GID: $.data[0].gid12// JSON Path for name: $.data[0].name1314// GET /projects?workspace={gid} response structure15{16 "data": [17 {18 "gid": "9876543210987654",19 "name": "Mobile App Redesign",20 "resource_type": "project"21 }22 ]23}24// JSON Path for list: $.data[*].gid and $.data[*].namePro tip: Workspace and project GIDs don't change, so you only need to fetch them once per session. Store them in App State on first load and reuse them for all subsequent task calls.
Expected result: GetWorkspaces and GetProjects API Calls are configured and tested successfully. You have your workspace and project GIDs stored in App State variables.
Fetch tasks with opt_fields and bind to a ListView
Now add the core API Call. Name it GetTasks. Method: GET. Endpoint: /tasks?project={{projectGid}}&opt_fields=name,due_on,assignee.name,completed,notes. Add a variable named projectGid of type String. The opt_fields parameter is critical. Without it, Asana returns only { "gid": "...", "name": "..." } for each task — every other field (due_on, assignee, completed, notes) will be completely absent from the response. There's no error; the fields are just not there. Binding $.data[*].due_on to a Text widget will show nothing, silently. Always include every field you plan to display in opt_fields. Go to the Response & Test tab. Set the projectGid variable to your project GID and click Test API Call. You should see tasks with the requested fields. Click Generate from Response to create JSON Paths. Key JSON Paths to map (note the $.data[*] prefix, not $[*]): - Task name: $.data[*].name - Due date: $.data[*].due_on - Assignee: $.data[*].assignee.name - Completed: $.data[*].completed (boolean) - Task GID: $.data[*].gid On your tasks page in FlutterFlow, add a ListView. Set its Backend Query to the Asana group → GetTasks, passing currentProjectGid from App State as the projectGid variable. Inside the ListView, add Card and Text widgets, binding each to the appropriate JSON Paths. For the due date, bind $.data[*].due_on — guard it with a conditional because tasks without a due date will have null here.
1// GET /tasks request — opt_fields is REQUIRED2// GET /tasks?project={projectGid}&opt_fields=name,due_on,assignee.name,completed,notes,gid34// Sample response (paste into Response & Test → Generate from Response)5{6 "data": [7 {8 "gid": "1234567890",9 "name": "Write onboarding copy",10 "due_on": "2026-07-15",11 "completed": false,12 "notes": "Review with marketing before finalizing",13 "assignee": {14 "gid": "9876543210",15 "name": "Jane Doe"16 },17 "resource_type": "task"18 },19 {20 "gid": "1234567891",21 "name": "Design login screen",22 "due_on": null,23 "completed": true,24 "notes": "",25 "assignee": null26 }27 ]28}2930// Key JSON Paths31// $.data[*].name → task name32// $.data[*].due_on → due date (string YYYY-MM-DD, can be null)33// $.data[*].assignee.name → assignee (can be null if unassigned)34// $.data[*].completed → boolean35// $.data[*].gid → task GID (needed for updates)Pro tip: If your ListView shows task names but due dates and assignees are always blank, you've missed opt_fields. Add ?opt_fields=name,due_on,assignee.name,completed to the endpoint and regenerate JSON Paths.
Expected result: The GetTasks API Call returns tasks with name, due_on, assignee.name, and completed fields. A ListView on your tasks page displays them with correct data bindings.
Create tasks and mark complete via secure API calls
Completing a task requires a PUT request to /tasks/{gid} with a body of {"data":{"completed":true}}. Creating a task requires a POST to /tasks with a body containing workspace GID, name, assignee, and project. Both are write operations. For an app with a small, trusted user base (e.g., an internal tool where users log in with your company Asana accounts), you might accept the security trade-off and make these calls directly from FlutterFlow. For consumer-facing apps, route writes through a Firebase Cloud Function or Supabase Edge Function to protect the PAT. If you're making direct FlutterFlow calls, add a CompleteTask API Call in the Asana group. Method: PUT. Endpoint: /tasks/{{taskGid}}. Add a taskGid variable. Set the body to {"data": {"completed": true}}. In your task ListView, add a Checkbox or IconButton to each task row. Wire its Action to Backend/API Call → CompleteTask, passing the current row's $.data[*].gid as taskGid. After a successful response, trigger a page refresh (or update the local App State to reflect the completion without a full refresh, for a snappier UX). For task creation, build a bottom sheet or new page with text fields for task name and due date. POST to /tasks with a body: {"data": {"name": "{{taskName}}", "projects": ["{{projectGid}}"], "workspace": "{{workspaceGid}}", "due_on": "{{dueDate}}", "assignee": "me"}}. The word 'me' in the assignee field automatically assigns to the token owner. Be mindful of the 1,500 req/min rate limit if you have many simultaneous users. If your app powers a team of 20 people all checking tasks at once, you may be sending 20 requests per page load — still well within limits for most team sizes.
1// PUT /tasks/{taskGid} — mark task complete2// Method: PUT3// Endpoint: /tasks/{{taskGid}}4// Body:5{6 "data": {7 "completed": true8 }9}1011// POST /tasks — create a new task12// Method: POST13// Endpoint: /tasks14// Body:15{16 "data": {17 "name": "{{taskName}}",18 "notes": "{{taskNotes}}",19 "due_on": "{{dueDate}}",20 "projects": ["{{projectGid}}"],21 "workspace": "{{workspaceGid}}",22 "assignee": "me"23 }24}25// Note: all body values are also wrapped in the 'data' envelope for writesPro tip: If you'd rather skip the backend setup and have RapidDev build this integration for you, we do FlutterFlow + Asana integrations regularly — free scoping call at rapidevelopers.com/contact.
Expected result: Tasks can be marked complete from the ListView and new tasks can be created from a form. The task list refreshes to reflect changes.
Common use cases
Team task dashboard for a cross-functional project
Build a mobile app where team members can see all open tasks in an Asana project, filtered by their own assignee. The app fetches tasks from a specific project GID with opt_fields including name, due_on, assignee.name, and completed. Tasks are displayed in a ListView grouped by due date with overdue items highlighted in red.
Show all incomplete tasks in my Asana project assigned to me, sorted by due date, with task name, due date, and a checkbox to mark complete.
Copy this prompt to try it in FlutterFlow
Client-facing project progress tracker
Build a read-only app for clients to check project progress. The app fetches all tasks in a designated client project, counts total vs completed tasks, and shows a progress bar plus a list of recent completions. The client can see what's done and what's in flight without needing a Jira account.
Display an Asana project's overall completion percentage and a list of tasks completed in the last 7 days with task name and completion date.
Copy this prompt to try it in FlutterFlow
Personal daily task companion app
Build a focused personal task app that shows only tasks assigned to the current user and due today or earlier. The app calls GET /tasks?assignee=me&workspace={gid}&due_on.before=today with opt_fields for name, due_on, projects.name and presents a compact checklist. Completing a task sends a PUT request to mark it done.
Show all Asana tasks assigned to me that are due today or overdue, with task name, project name, and a button to mark each one complete.
Copy this prompt to try it in FlutterFlow
Troubleshooting
ListView shows task names but due dates, assignees, and other fields are always blank
Cause: The opt_fields query parameter is missing or doesn't include the fields being bound. Asana only returns gid and name by default; every additional field must be explicitly requested via opt_fields.
Solution: Update your GetTasks API Call endpoint to include opt_fields: /tasks?project={{projectGid}}&opt_fields=name,due_on,assignee.name,completed,notes,gid. Click Test API Call to confirm the new fields appear in the response, then regenerate JSON Paths.
ListView is always empty despite the API returning data in the Response tab
Cause: JSON Path bindings are missing the .data prefix. Asana wraps all responses in a 'data' envelope: { "data": [...] }. Bindings starting with $[*] instead of $.data[*] won't match any data.
Solution: Check all your JSON Path bindings for this API Call. Change $[*].name to $.data[*].name, $[*].gid to $.data[*].gid, and so on for every field. Re-generate JSON Paths from the Response & Test tab to ensure they start with $.data[*].
429 Too Many Requests error when loading a dashboard with multiple projects
Cause: The app is making one task-fetch call per project on page load. With 10+ projects, this fans out to 10+ concurrent requests, which can be a risk at high traffic volumes even with the 1,500 req/min PAT limit.
Solution: Load project GIDs into a selector/picker first and only fetch tasks for the project the user actively selects. Store loaded task lists in App State so navigating away and back doesn't re-fetch. Show a 'Tap to load' button rather than auto-loading all projects at once.
401 Unauthorized — token appears correct
Cause: The PAT may have been revoked, the Bearer prefix may be missing, or there's a trailing space or newline in the token value copied from Asana's UI.
Solution: Go to asana.com/0/my-apps, revoke the old token, and create a new one. Paste the new token carefully without trailing spaces. In your App Constant, ensure the Authorization header value is exactly: Bearer [token] — the word Bearer, a single space, then the token with no surrounding spaces or newlines.
Best practices
- Always include opt_fields in every task/project API Call — omitting it returns only gid and name, and missing field bindings fail silently without any error message.
- Remember the 'data' envelope: every Asana response wraps results in { "data": ... } so all JSON Path bindings must start with $.data, not $[*] or $.results.
- Fetch workspace and project GIDs once on app load and store them in App State — GIDs don't change, so re-fetching them on every navigation is wasteful.
- Guard all nullable field bindings (due_on, assignee) with Conditional Widgets so null values show a placeholder ('No due date', 'Unassigned') rather than a blank or crash.
- Use server-side filtering via the project and assignee query parameters rather than fetching all tasks and filtering in Dart — this keeps response sizes small and respects rate limits.
- When building for a team, test with 5-10 simultaneous simulated page loads to confirm your request rate stays comfortably below the 1,500 req/min limit per PAT.
- Display task GIDs in App State when navigating to detail pages so you can construct PUT/DELETE endpoints without an extra lookup call.
- Avoid polling Asana on a tight interval; use explicit pull-to-refresh gestures instead to keep request counts low.
Alternatives
Choose Trello if you need a kanban board visual and prefer simpler flat JSON responses without the 'data' envelope or opt_fields complexity — Trello authenticates via URL parameters rather than Bearer headers.
Choose Todoist for personal or small-team task tracking — its API is simpler with flat array responses, no GID chaining, and server-side filter syntax that handles most query patterns without multiple API calls.
Frequently asked questions
Why does FlutterFlow show empty task lists when the API clearly returns data?
The most likely cause is incorrect JSON Path bindings. Every Asana response is wrapped in a 'data' key — a task list returns { "data": [ {...}, {...} ] }, not a bare array. Your JSON Path bindings must start with $.data[*] not $[*]. Open the Response & Test tab for your GetTasks call, paste a sample response, click Generate from Response, and FlutterFlow will generate the correct paths automatically.
What is opt_fields and why is it required?
opt_fields is an Asana API query parameter that tells the server which fields to include in the response. Without it, Asana returns only the gid and name of each object to minimize payload size. If you want due_on, assignee.name, completed, or notes in your response, you must request them explicitly: ?opt_fields=name,due_on,assignee.name,completed. Missing opt_fields causes silent null values in your UI — no error, just empty fields.
Can I connect FlutterFlow to Asana without writing any custom code?
Yes, for read-only operations. The API Group with Bearer auth and GET calls to /workspaces, /projects, and /tasks can be set up entirely through FlutterFlow's visual API Calls panel without any Dart code. You only need a Custom Function if you want to format dates or handle nested opt_fields responses. Write operations (creating or completing tasks) can also be done directly in the API Calls panel, though routing them through a Cloud Function is recommended for security.
Does this integration work on both iOS/Android and web FlutterFlow builds?
Yes, for mobile builds (iOS and Android). For web builds, browser CORS policies may block direct calls to app.asana.com. Asana's API does include CORS headers for common origins, but if you encounter XMLHttpRequest errors on your published web build, route API calls through a Firebase Cloud Function that sets permissive CORS headers.
How do I show tasks from multiple Asana projects on one screen?
Make one GetTasks call per project and merge the results in App State. Use a Sequential action to call GetProjects, iterate through the results, call GetTasks for each project GID, and append results to an App State list. Be mindful of the 1,500 req/min rate limit — fetching 15 projects at once is 15 calls which is still well within limits, but avoid running this sequence on a polling timer.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation