0

This is somewhat a lengthy post about the Asana and it's very "not-that-flexible" and "things-left-to-wish-for" API and if duplicating a project can be done in any other way then this.

First off i must state that i like Asana and working in it through their website. So i am not against it or have any quarrel with it except with their API. If you haven't, you should try it UNLESS you want/have to write something that targets the Asana platform through their API.


I posted a question earlier this week about duplicating a project that you can find here: Copy an Asana task/project

Question was simply, can you duplicate a project or a group of tasks with the Asana API. The answer from Agnoster was simply that this was not possible right now but it's in the backlog.

So i wrote some code that actually copies the project (see below) and then recreates the entire project with all tasks and subtasks. This was a rather expensive process since you have to query each task seperatly, meaning that if i have 10 tasks it would be 1+10 queries. One query to get the information that the project contained 10 tasks and then i had to query each and every task to see if it contained any subtasks. If you would have one subtask in each of those tasks listed above then you would have to query their API 1+10*2 times. Imagine if you had deep layers of nested subtasks then you could potentially end up with several hundred or even a thousand queries in the end.

We currently have (in the project i am duplicating) 50 tasks and only one level of subtasks on a few of them, meaning roughly 1+50+50 requests. So a total of around 101 queries to their API.

Now i discovered that the tasks i am querying from Asana lacks information.

I am using this: "/projects/{project-id}/tasks" to get a list of tasks in order to make a duplicate of each and everyone of those tasks. Then i need to query each task this "tasks/{task-id}/subtasks" in order to get each subtask. Pretty straight forward.

Now the problem lies with their API and what i get delivered back from it and here i hope that there is a better solution at hands.

If i query this: "/projects/{project-id}/tasks" i get the following result:

{
  "data": [
    {
      "id": 1248,
      "name": "Buy catnip"
    },
    {
      "id": 24816,
      "name": "Reflect on role of kittens in society"
    },
    {
      ...
    }
  ]
}

Does anyone see the problem here? Namely the fact that i only get the ID and Name back from the server and nothing else. A task consists of notes, an assignee, date created, due date etc but that information is not shipped along in the list above and the same goes for the API endpoint "tasks/{task-id}/subtasks".

So where does that leave us? Well i would have to query each and every task a second time in order to get the information that is associated with the actual task. So my already huge amount of queries is about to get almost doubled. My 1+50+50 queries will end up to be 1+50+50+50+50. So in order to duplicate this project through their API i would be slightly over 200 queries in order to get all information from the first project over to the second, but wait, there is more. This is just the number of queries i need to actually query the tasks. Then you would have to add another 100 queries in order to create all the new tasks in the new project.

So end effect is that my small project that i want to duplicate to a new project through Asanas API will cost me roughly 301 queries to achive. Even a tiny update that indicates if a task has subtasks would have reduced the number of queries significantly. (Example: { "id": 1248, "name": "Buy catnip", "hasSubtasks": false })

My question is simple, is there any way to improve this? Have i missed some vital API endpoints that could save queries and improve performance?

Here is the code that i wrote to create this:

    public bool DuplicateAsanaProject(string projectId)
    {
        // Get the current project
        var request = new RestRequest(string.Format("/projects/{0}", projectId), Method.GET);
        request.RequestFormat = DataFormat.Json;

        var asanaProject = Execute<AsanaProjectTemplate>(request);
        if (asanaProject == null)
            return false;

        // Create a new project
        var newAsanaProject = CreateAsanaProjectInTeam(asanaProject.Data.Name, asanaProject.Data.Notes, TeamProductionId);
        // Get all the tasks in the current project
        var tasksInProject = GetAsanaTasksForProject(asanaProject.Data.Id);
        // Reverse all the tasks to get them outputted in the correct order
        tasksInProject.Data.Reverse();
        // Loop through all project tasks
        foreach (var task in tasksInProject.Data)
        {
            // Get the task from the server
            var oldTask = GetAsanaTask(task.Id);
            // Create the new task
            var newTask = CreateAsanaTask(oldTask.Data.Name, oldTask.Data.Notes, newAsanaProject.Data.Id, WorkspaceId);

            // Get the sub-tasks for this task
            var tasksList = GetAsanaTasksForTask(task.Id);
            // If we have some sub-tasks, then create them
            if (tasksList != null && tasksList.Data.Count > 0)
                CreateSubTasksForTask(newTask.Data.Id, tasksList);
        }
        return true;            
    }

    /// <summary>
    /// Create sub-tasks for a specific task
    /// </summary>
    /// <param name="taskId">New task id. This is the task in where the new tasks will be added</param>
    /// <param name="tasks">List all sub-tasks</param>
    public void CreateSubTasksForTask(string taskId, AsanaTasksTemplate tasks)
    {
        // Reverse all the tasks to get them outputted in the correct order
        tasks.Data.Reverse();
        foreach(var task in tasks.Data)
        {
            // Get the task from the server
            var oldTask = GetAsanaTask(task.Id);
            // Create the new task
            var newTask = CreateAsanaSubTask(oldTask.Data.Name, oldTask.Data.Notes, taskId);

            // Get the sub-tasks for this task
            var tasksList = GetAsanaTasksForTask(task.Id);
            // If we have some sub-tasks, then create them
            if (tasksList != null && tasksList.Data.Count > 0)
                CreateSubTasksForTask(newTask.Data.Id, tasksList);
        }
    }

    /// <summary>
    /// Retrieves all sub-tasks for a specific task
    /// </summary>
    /// <param name="taskId"></param>
    /// <returns></returns>
    public AsanaTasksTemplate GetAsanaTasksForTask(string taskId)
    {
        // Get all tasks in the current task
        var request = new RestRequest(string.Format("tasks/{0}/subtasks", taskId), Method.GET);
        request.RequestFormat = DataFormat.Json;

        var result = Execute<AsanaTasksTemplate>(request);
        return result;
    }
Community
  • 1
  • 1
Patrick
  • 5,442
  • 9
  • 53
  • 104

1 Answers1

1

The short answer is, yes! There is a way to control the data you get back, and it's called opt_fields. So, for example:

GET /projects/ID/tasks?opt_fields=name,notes,created_at,assignee.name,subtasks.name,subtasks.subtasks.name,subtasks.subtasks.subtasks.name

This would return - all tasks in the project - with their name, notes, creation time - the assignee and their name - each nested subtask up to 3 levels deep with name only

Alternatively, opt_expand=. would give all the "standard" fields for a task that you would ordinarily get back if you requested just that task.

Both opt_expand and opt_fields are documented in the Input/Output options section of the developer docs.

agnoster
  • 3,744
  • 2
  • 21
  • 29
  • Thanks, that reduced the calls with 100 (in my case above). It's still going to be around 200 calls to accomplish my duplication of the above mentioned project, corret? – Patrick Oct 13 '14 at 11:09
  • Assuming you can craft a single query to get all the information you need, it should be a total of 101 queries - 1 GET to fetch all the information, then 100 individual POST requests to create each task (if I'm understanding your goal correctly). In practice, however, that GET is going to get a lot of data, so you might still want to use [pagination](http://developer.asana.com/documentation-preview/#Pagination) to only get chunk, say 25, at a time - still would cut down 200 requests to 4. – agnoster Oct 13 '14 at 16:14
  • It sounds to me like you really just need to figure out what fields you need, and then request them via `?opt_fields` for the top level and the subtask level (since you mentioned you only have one level of subtasks). So, for example, if you just want names and notes, you would use `?opt_fields=name,notes,subtasks.name,subtasks.notes`. This eliminates needing to make all the nested requests. If there's a rare case where tasks may be nested two levels deep, you could also include `subtasks.subtasks.id` and then fetch those manually if you find any. – agnoster Oct 13 '14 at 16:16
  • Thanks for the response, that clarifies things. I will look forward to the API endpoint to duplicate tasks and projects =) – Patrick Oct 14 '14 at 09:36
  • Well, bear in mind we have no concrete plans - there's *lots* of stuff in the API we're working on improving, and duplicating tasks/projects is a bit of a niche feature, so I wouldn't plan on it existing any time soon. We try to focus on API features that create the most wide use cases. Like, in this case the broader use case might be a batch-create endpoint, which would both help in your case get to a total of 2 calls (one get, one post), and also help in a variety of other cases. At any rate, I hope you can now do all your GETs in 1 (or at least very few) calls! – agnoster Oct 14 '14 at 14:28