# Asana Client

Create tasks, manage projects, and track work in Asana.

## Methods

| Method                                   | Description                                       |
| ---------------------------------------- | ------------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any Asana API request with schema validation |

## Usage

### Create a Task

```typescript
import { api, z, asana } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_ASANA = "a1b2c3d4-5678-90ab-cdef-asana0000001";

const TaskSchema = z.object({
  gid: z.string(),
  name: z.string(),
  notes: z.string(),
  completed: z.boolean(),
  due_on: z.string().nullable(),
  assignee: z.object({ gid: z.string(), name: z.string() }).nullable(),
  projects: z.array(z.object({ gid: z.string(), name: z.string() })),
});

const CreateTaskResponseSchema = z.object({
  data: TaskSchema,
});

export default api({
  name: "AsanaExample",
  integrations: {
    asana: asana(PROD_ASANA),
  },
  input: z.object({
    projectId: z.string(),
    name: z.string(),
    description: z.string(),
  }),
  output: z.object({
    taskId: z.string(),
  }),
  async run(ctx, { projectId, name, description }) {
    const result = await ctx.integrations.asana.apiRequest(
      {
        method: "POST",
        path: "/tasks",
        body: {
          data: {
            name: name,
            notes: description,
            projects: [projectId],
          },
        },
      },
      { response: CreateTaskResponseSchema },
    );

    return { taskId: result.data.gid };
  },
});
```

### Get Tasks in a Project

```typescript
const ListTasksResponseSchema = z.object({
  data: z.array(
    z.object({
      gid: z.string(),
      name: z.string(),
      completed: z.boolean(),
    }),
  ),
  next_page: z
    .object({
      offset: z.string(),
      uri: z.string(),
    })
    .nullable(),
});

const result = await ctx.integrations.asana.apiRequest(
  {
    method: "GET",
    path: `/projects/${projectId}/tasks`,
    params: {
      opt_fields: "name,completed,due_on,assignee.name",
      completed_since: "now", // Only incomplete tasks
    },
  },
  { response: ListTasksResponseSchema },
);

result.data.forEach((task) => {
  console.log(`${task.completed ? "✓" : "○"} ${task.name}`);
});
```

### Update a Task

```typescript
const result = await ctx.integrations.asana.apiRequest(
  {
    method: "PUT",
    path: `/tasks/${taskId}`,
    body: {
      data: {
        completed: true,
        notes: "Task completed successfully",
      },
    },
  },
  { response: z.object({ data: TaskSchema }) },
);
```

### Assign a Task

```typescript
const result = await ctx.integrations.asana.apiRequest(
  {
    method: "PUT",
    path: `/tasks/${taskId}`,
    body: {
      data: {
        assignee: userId, // User GID
        due_on: "2024-12-31",
      },
    },
  },
  { response: z.object({ data: TaskSchema }) },
);
```

### List Projects

```typescript
const ListProjectsResponseSchema = z.object({
  data: z.array(
    z.object({
      gid: z.string(),
      name: z.string(),
      archived: z.boolean(),
    }),
  ),
});

const result = await ctx.integrations.asana.apiRequest(
  {
    method: "GET",
    path: "/projects",
    params: {
      workspace: workspaceId,
      opt_fields: "name,archived,owner.name",
      archived: false,
    },
  },
  { response: ListProjectsResponseSchema },
);
```

### Create a Project

```typescript
const CreateProjectResponseSchema = z.object({
  data: z.object({
    gid: z.string(),
    name: z.string(),
  }),
});

const result = await ctx.integrations.asana.apiRequest(
  {
    method: "POST",
    path: "/projects",
    body: {
      data: {
        name: "Q1 Marketing Campaign",
        workspace: workspaceId,
        team: teamId, // Required for team workspaces
        default_view: "list",
        notes: "Project description here",
      },
    },
  },
  { response: CreateProjectResponseSchema },
);
```

### Add a Comment to a Task

```typescript
const StoryResponseSchema = z.object({
  data: z.object({
    gid: z.string(),
    text: z.string(),
    created_at: z.string(),
  }),
});

const result = await ctx.integrations.asana.apiRequest(
  {
    method: "POST",
    path: `/tasks/${taskId}/stories`,
    body: {
      data: {
        text: "Great progress on this task!",
      },
    },
  },
  { response: StoryResponseSchema },
);
```

### Search Tasks

```typescript
const SearchResponseSchema = z.object({
  data: z.array(
    z.object({
      gid: z.string(),
      name: z.string(),
    }),
  ),
});

const result = await ctx.integrations.asana.apiRequest(
  {
    method: "GET",
    path: "/workspaces/{workspace_gid}/tasks/search",
    params: {
      "text.value": "urgent",
      completed: false,
      "assignee.any": userId,
      sort_by: "due_date",
      sort_ascending: true,
    },
  },
  { response: SearchResponseSchema },
);
```

### Get Subtasks

```typescript
const result = await ctx.integrations.asana.apiRequest(
  {
    method: "GET",
    path: `/tasks/${taskId}/subtasks`,
    params: {
      opt_fields: "name,completed,assignee.name",
    },
  },
  { response: ListTasksResponseSchema },
);
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await asana.createTask({ ... });
await asana.getTasks({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.asana.apiRequest(
  { method: "POST", path: "/tasks", body: { data: { ... } } },
  { response: CreateTaskResponseSchema }
);
```

### Data Wrapper

Asana wraps request bodies and responses in a `data` object:

```typescript
// Request body
const body = {
  data: {
    // Wrapped in data
    name: "Task name",
    notes: "Description",
  },
};

// Response
const schema = z.object({
  data: TaskSchema, // Wrapped in data
});
```

### opt_fields Parameter

Use `opt_fields` to specify which fields to return:

```typescript
// Without opt_fields, you get minimal data
const params = {};

// With opt_fields, you get specific fields
const params = {
  opt_fields: "name,completed,due_on,assignee.name,projects.name",
};

// Nested fields use dot notation
// assignee.name returns { assignee: { gid, name } }
```

### GID vs ID

Asana uses `gid` (Global ID) as the identifier:

```typescript
// WRONG - Using "id"
const task = { id: "123456" };

// CORRECT - Using "gid"
const task = { gid: "123456" };
```

### Workspace vs Team

- **Workspace**: Container for all projects and tasks
- **Team**: Required when creating projects in organizations

```typescript
// Personal workspace - workspace only
const body = {
  data: {
    name: "Project",
    workspace: workspaceId,
  },
};

// Organization - workspace and team required
const body = {
  data: {
    name: "Project",
    workspace: workspaceId,
    team: teamId,
  },
};
```

### Date Formats

Asana uses ISO date format (YYYY-MM-DD):

```typescript
const body = {
  data: {
    due_on: "2024-12-31", // Date only
    due_at: "2024-12-31T17:00:00.000Z", // Date and time
    start_on: "2024-12-01",
  },
};
```

### Pagination

Large result sets require pagination:

```typescript
async function getAllTasks(asana: AsanaClient, projectId: string) {
  const allTasks: Task[] = [];
  let offset: string | undefined;

  do {
    const result = await ctx.integrations.asana.apiRequest(
      {
        method: "GET",
        path: `/projects/${projectId}/tasks`,
        params: {
          limit: 100,
          ...(offset && { offset }),
        },
      },
      { response: ListTasksResponseSchema },
    );

    allTasks.push(...result.data);
    offset = result.next_page?.offset;
  } while (offset);

  return allTasks;
}
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.asana.apiRequest(
    { method: "POST", path: "/tasks", body: { data: { ... } } },
    { response: CreateTaskResponseSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Asana API Documentation](https://developers.asana.com/docs)
- [Tasks](https://developers.asana.com/reference/tasks)
- [Projects](https://developers.asana.com/reference/projects)
- [Search](https://developers.asana.com/reference/searchtasksforworkspace)
