PHPackages                             wmbh/laravel-asana - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. wmbh/laravel-asana

ActiveLibrary[API Development](/categories/api)

wmbh/laravel-asana
==================

A Laravel package for interacting with the Asana API

v0.1.0(2mo ago)035↑75%[1 PRs](https://github.com/WMBH/laravel-asana/pulls)MITPHPPHP ^8.3CI passing

Since Feb 25Pushed 2mo agoCompare

[ Source](https://github.com/WMBH/laravel-asana)[ Packagist](https://packagist.org/packages/wmbh/laravel-asana)[ Docs](https://github.com/WMBH/laravel-asana)[ RSS](/packages/wmbh-laravel-asana/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (18)Versions (6)Used By (0)

Laravel Asana
=============

[](#laravel-asana)

[![Latest Version on Packagist](https://camo.githubusercontent.com/306282ba5406b5b2dc36cab4ff0b8fe5b7d9976db75aee1f6245676cf7805bc6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f776d62682f6c61726176656c2d6173616e612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/wmbh/laravel-asana)[![GitHub Tests Action Status](https://camo.githubusercontent.com/5c87fb200bde065a4d55a878ee143a90d2e7a155213fe3578372253f7f3640b9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f776d62682f6c61726176656c2d6173616e612f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/wmbh/laravel-asana/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/38719037231ca22718eeb6a081e8ebfed6318dd04cfc9de942bafe814753c070/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f776d62682f6c61726176656c2d6173616e612f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/wmbh/laravel-asana/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/5d6b13af303744ca0de245fcb37b6f37ec24032193007549e39281679f1800a8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f776d62682f6c61726176656c2d6173616e612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/wmbh/laravel-asana)

A comprehensive Laravel package for the Asana REST API, built with [Saloon](https://docs.saloon.dev/) and [Spatie Laravel Data](https://spatie.be/docs/laravel-data).

Installation
------------

[](#installation)

```
composer require wmbh/laravel-asana
```

Publish the config file:

```
php artisan vendor:publish --tag="asana-config"
```

Config contents:

```
return [
    'token' => env('ASANA_TOKEN'),
    'timeout' => env('ASANA_TIMEOUT', 30),
    'retry' => [
        'attempts' => env('ASANA_RETRY_ATTEMPTS', 3),
        'sleep' => env('ASANA_RETRY_SLEEP', 1000),
    ],
];
```

Add your Asana Personal Access Token to `.env`:

```
ASANA_TOKEN=your-asana-pat-here

```

Quick Start
-----------

[](#quick-start)

```
# Verify your token works
php artisan asana:test
```

```
use WMBH\Asana\Facades\Asana;

$me = Asana::users()->me();
$tasks = Asana::tasks()->getForProject('project_gid');
$task = Asana::tasks()->create([
    'name' => 'My task',
    'workspace' => 'workspace_gid',
]);
```

---

API Reference
-------------

[](#api-reference)

All methods are accessed through the `Asana` facade. Each resource returns typed DTOs (Spatie laravel-data objects) or `PaginatedResponse` for collections.

### Table of Contents

[](#table-of-contents)

- [Tasks](#tasks)
- [Task Search (Query Builder)](#task-search-query-builder)
- [Projects](#projects)
- [Sections](#sections)
- [Users](#users)
- [Workspaces](#workspaces)
- [Teams](#teams)
- [Tags](#tags)
- [Stories (Comments)](#stories-comments)
- [Attachments](#attachments)
- [Custom Fields](#custom-fields)
- [Portfolios](#portfolios)
- [Goals](#goals)
- [Webhooks](#webhooks)
- [Batch Requests](#batch-requests)
- [Error Handling](#error-handling)
- [Pagination](#pagination)

---

### Tasks

[](#tasks)

Access via `Asana::tasks()` — returns `TaskResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``TaskData`Get a single task`getForProject``string $projectGid`, `array $optFields = []`, `?string $offset = null`, `?int $limit = null``PaginatedResponse`List tasks in a project`getForSection``string $sectionGid`, `array $optFields = []``PaginatedResponse`List tasks in a section`getSubtasks``string $taskGid`, `array $optFields = []``PaginatedResponse`List subtasks of a task`create``array $data``TaskData`Create a new task`update``string $gid`, `array $data``TaskData`Update a task`delete``string $gid``bool`Delete a task`search``string $workspaceGid`, `array $params = []``TaskQueryBuilder` or `PaginatedResponse`Search tasks (see [Query Builder](#task-search-query-builder))`addTag``string $taskGid`, `string $tagGid``void`Add a tag to a task`removeTag``string $taskGid`, `string $tagGid``void`Remove a tag from a task`addFollowers``string $taskGid`, `array $followers``void`Add followers to a task`addProject``string $taskGid`, `string $projectGid`, `?string $sectionGid = null`, `?string $insertBefore = null`, `?string $insertAfter = null``void`Add task to a project`removeProject``string $taskGid`, `string $projectGid``void`Remove task from a project`setParent``string $taskGid`, `string $parentGid``TaskData`Set a task's parent`getDependencies``string $taskGid``PaginatedResponse`Get task dependencies`getDependents``string $taskGid``PaginatedResponse`Get task dependents`addDependencies``string $taskGid`, `array $dependencyGids``void`Add dependencies to a task`addDependents``string $taskGid`, `array $dependentGids``void`Add dependents to a task```
use WMBH\Asana\Facades\Asana;

// Get a task with specific fields
$task = Asana::tasks()->get('task_gid', ['name', 'due_on', 'assignee']);

// List tasks in a project with pagination
$page = Asana::tasks()->getForProject('project_gid', limit: 25);
// $page->data contains TaskData[]
// $page->hasNextPage() / $page->nextPageToken

// Create a task
$task = Asana::tasks()->create([
    'name' => 'My new task',
    'workspace' => 'workspace_gid',
    'assignee' => 'me',
    'due_on' => '2025-03-01',
    'notes' => 'Task description here',
]);

// Update a task
$task = Asana::tasks()->update('task_gid', [
    'name' => 'Updated name',
    'completed' => true,
]);

// Delete a task
Asana::tasks()->delete('task_gid');

// Relationships
Asana::tasks()->addTag('task_gid', 'tag_gid');
Asana::tasks()->removeTag('task_gid', 'tag_gid');
Asana::tasks()->addFollowers('task_gid', ['user_gid_1', 'user_gid_2']);
Asana::tasks()->addProject('task_gid', 'project_gid', sectionGid: 'section_gid');
Asana::tasks()->removeProject('task_gid', 'project_gid');
Asana::tasks()->setParent('task_gid', 'parent_task_gid');

// Dependencies
Asana::tasks()->addDependencies('task_gid', ['blocker_task_1', 'blocker_task_2']);
Asana::tasks()->addDependents('task_gid', ['blocked_task_1']);
$deps = Asana::tasks()->getDependencies('task_gid');
```

#### TaskData Properties

[](#taskdata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"task"``name``?string`Task name`resource_subtype``?string``"default_task"`, `"milestone"`, `"section"`, or `"approval"``assignee``?CompactResource`Assigned user (`->gid`, `->name`)`assignee_section``?CompactResource`Assignee's board column`completed``?bool`Whether the task is completed`completed_at``?string`Completion timestamp`created_at``?string`Creation timestamp`due_on``?string`Due date (`YYYY-MM-DD`)`due_at``?string`Due datetime (ISO 8601)`start_on``?string`Start date`start_at``?string`Start datetime`modified_at``?string`Last modified timestamp`notes``?string`Plain-text description`html_notes``?string`HTML description`num_hearts``?int`Number of hearts`num_likes``?int`Number of likes`is_rendered_as_separator``?bool`Rendered as separator in list view`parent``?CompactResource`Parent task`workspace``?CompactResource`Workspace`permalink_url``?string`URL to the task in Asana`tags``?array`Tags on the task`projects``?array`Projects the task belongs to`memberships``?array`Project memberships`followers``?array`Users following the task`custom_fields``?array`Custom field values---

### Task Search (Query Builder)

[](#task-search-query-builder)

Calling `search()` with no params returns a fluent `TaskQueryBuilder`:

```
$tasks = Asana::tasks()->search('workspace_gid')
    ->assignee('me')
    ->completed(false)
    ->dueAfter('2025-01-01')
    ->dueBefore('2025-12-31')
    ->sortBy('due_on')
    ->fields('name', 'due_on', 'assignee')
    ->limit(50)
    ->get();
```

MethodParametersReturnsDescription`where``string $field`, `mixed $value``static`Set an arbitrary search param`assignee``string $assigneeGid``static`Filter by assignee (`'me'` or user GID)`project``string $projectGid``static`Filter by project`section``string $sectionGid``static`Filter by section`tag``string $tagGid``static`Filter by tag`completed``bool $completed = true``static`Filter by completion status`modifiedSince``string $datetime``static`Tasks modified after datetime`dueOn``string $date``static`Tasks due on date (`YYYY-MM-DD`)`dueBefore``string $date``static`Tasks due before date`dueAfter``string $date``static`Tasks due after date`sortBy``string $field`, `bool $ascending = true``static`Sort results (`due_on`, `created_at`, `completed_at`, `likes`, `modified_at`)`fields``string ...$fields``static`Specify which fields to return (`opt_fields`)`limit``int $limit``static`Max results to return`get`—`Collection`Execute search, return `Collection` of `TaskData``paginate`—`PaginatedResponse`Execute search, return paginated responseYou can also pass params directly to skip the builder:

```
$results = Asana::tasks()->search('workspace_gid', [
    'assignee.any' => 'me',
    'completed' => false,
    'opt_fields' => 'name,due_on',
]);
// Returns PaginatedResponse directly
```

---

### Projects

[](#projects)

Access via `Asana::projects()` — returns `ProjectResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``ProjectData`Get a project`list``string $workspaceGid`, `array $optFields = []`, `?string $offset = null`, `?int $limit = null``PaginatedResponse`List projects in a workspace`getForTeam``string $teamGid`, `array $optFields = []`, `?string $offset = null`, `?int $limit = null``PaginatedResponse`List projects for a team`create``array $data``ProjectData`Create a project`update``string $gid`, `array $data``ProjectData`Update a project`delete``string $gid``bool`Delete a project`duplicate``string $gid`, `array $data``array`Duplicate a project (returns job)`getTaskCounts``string $gid``array`Get task count breakdown```
// List projects in a workspace
$projects = Asana::projects()->list('workspace_gid');

// Get a project
$project = Asana::projects()->get('project_gid');

// Create a project
$project = Asana::projects()->create([
    'name' => 'Q1 Sprint',
    'workspace' => 'workspace_gid',
    'team' => 'team_gid',
    'notes' => 'Sprint planning project',
    'default_view' => 'board',
]);

// Update a project
$project = Asana::projects()->update('project_gid', [
    'name' => 'Q1 Sprint (Updated)',
    'archived' => true,
]);

// Delete a project
Asana::projects()->delete('project_gid');

// Duplicate a project
$job = Asana::projects()->duplicate('project_gid', [
    'name' => 'Copy of Q1 Sprint',
    'include' => ['members', 'task_notes', 'task_assignee', 'task_subtasks'],
]);

// Get task counts
$counts = Asana::projects()->getTaskCounts('project_gid');
// ['num_tasks' => 42, 'num_completed_tasks' => 10, ...]

// List projects for a team
$projects = Asana::projects()->getForTeam('team_gid');
```

#### ProjectData Properties

[](#projectdata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"project"``name``?string`Project name`archived``?bool`Whether the project is archived`color``?string`Color of the project`created_at``?string`Creation timestamp`current_status``?array`Deprecated project status`current_status_update``?array`Latest status update`default_view``?string``"list"`, `"board"`, `"calendar"`, or `"timeline"``due_on``?string`Due date`due_date``?string`Due date (alias)`start_on``?string`Start date`modified_at``?string`Last modified timestamp`notes``?string`Plain-text description`html_notes``?string`HTML description`public``?bool`Whether visible to workspace`owner``?CompactResource`Project owner`team``?CompactResource`Team the project belongs to`workspace``?CompactResource`Workspace`permalink_url``?string`URL to the project in Asana`custom_fields``?array`Custom field values`members``?array`Project members`followers``?array`Project followers---

### Sections

[](#sections)

Access via `Asana::sections()` — returns `SectionResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``SectionData`Get a section`getForProject``string $projectGid`, `array $optFields = []``PaginatedResponse`List sections in a project`create``string $projectGid`, `array $data``SectionData`Create a section in a project`update``string $gid`, `array $data``SectionData`Update a section`delete``string $gid``bool`Delete a section`addTask``string $sectionGid`, `string $taskGid``void`Add a task to a section`insertSection``string $projectGid`, `array $data``void`Reorder a section within a project```
// List sections in a project
$sections = Asana::sections()->getForProject('project_gid');

// Create a section
$section = Asana::sections()->create('project_gid', [
    'name' => 'In Progress',
]);

// Move a task into a section
Asana::sections()->addTask('section_gid', 'task_gid');

// Reorder a section
Asana::sections()->insertSection('project_gid', [
    'section' => 'section_gid',
    'before_section' => 'other_section_gid',
]);

// Rename a section
Asana::sections()->update('section_gid', ['name' => 'Done']);

// Delete a section
Asana::sections()->delete('section_gid');
```

#### SectionData Properties

[](#sectiondata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"section"``name``?string`Section name`created_at``?string`Creation timestamp`project``?CompactResource`Parent project---

### Users

[](#users)

Access via `Asana::users()` — returns `UserResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``UserData`Get a user`list``array $optFields = []`, `?string $offset = null`, `?int $limit = null``PaginatedResponse`List all users`getForWorkspace``string $workspaceGid`, `array $optFields = []`, `?string $offset = null`, `?int $limit = null``PaginatedResponse`List users in a workspace`getForTeam``string $teamGid`, `array $optFields = []``PaginatedResponse`List users in a team`me``array $optFields = []``UserData`Get the authenticated user```
// Get the authenticated user
$me = Asana::users()->me();
echo $me->name;  // "John Doe"
echo $me->email; // "john@example.com"

// Get a specific user
$user = Asana::users()->get('user_gid');

// List users in a workspace
$users = Asana::users()->getForWorkspace('workspace_gid');

// List users in a team
$users = Asana::users()->getForTeam('team_gid');
```

#### UserData Properties

[](#userdata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"user"``name``?string`User's full name`email``?string`User's email address`photo``?array`Photo URLs at various sizes`workspaces``?array`Workspaces the user belongs to---

### Workspaces

[](#workspaces)

Access via `Asana::workspaces()` — returns `WorkspaceResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``WorkspaceData`Get a workspace`list``array $optFields = []`, `?string $offset = null`, `?int $limit = null``PaginatedResponse`List all workspaces`update``string $gid`, `array $data``WorkspaceData`Update a workspace`addUser``string $workspaceGid`, `string $userGid``void`Add a user to a workspace`removeUser``string $workspaceGid`, `string $userGid``void`Remove a user from a workspace```
// List all workspaces
$workspaces = Asana::workspaces()->list();

// Get a workspace
$workspace = Asana::workspaces()->get('workspace_gid');
echo $workspace->name;
echo $workspace->is_organization; // true/false

// Update a workspace
Asana::workspaces()->update('workspace_gid', ['name' => 'New Name']);

// Manage members
Asana::workspaces()->addUser('workspace_gid', 'user_gid');
Asana::workspaces()->removeUser('workspace_gid', 'user_gid');
```

#### WorkspaceData Properties

[](#workspacedata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"workspace"``name``?string`Workspace name`is_organization``?bool`Whether it's an organization`email_domains``?array`Email domains for the workspace---

### Teams

[](#teams)

Access via `Asana::teams()` — returns `TeamResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``TeamData`Get a team`getForWorkspace``string $workspaceGid`, `array $optFields = []`, `?string $offset = null`, `?int $limit = null``PaginatedResponse`List teams in a workspace`getForUser``string $userGid`, `string $organizationGid`, `array $optFields = []``PaginatedResponse`List teams for a user in an org`create``array $data``TeamData`Create a team`addUser``string $teamGid`, `string $userGid``void`Add a user to a team`removeUser``string $teamGid`, `string $userGid``void`Remove a user from a team```
// List teams in a workspace
$teams = Asana::teams()->getForWorkspace('workspace_gid');

// Get teams for a user
$teams = Asana::teams()->getForUser('user_gid', 'organization_gid');

// Create a team
$team = Asana::teams()->create([
    'name' => 'Engineering',
    'organization' => 'org_gid',
    'description' => 'The engineering team',
]);

// Manage members
Asana::teams()->addUser('team_gid', 'user_gid');
Asana::teams()->removeUser('team_gid', 'user_gid');
```

#### TeamData Properties

[](#teamdata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"team"``name``?string`Team name`description``?string`Plain-text description`html_description``?string`HTML description`organization``?CompactResource`Parent organization`permalink_url``?string`URL to the team in Asana---

### Tags

[](#tags)

Access via `Asana::tags()` — returns `TagResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``TagData`Get a tag`getForTask``string $taskGid`, `array $optFields = []``PaginatedResponse`List tags on a task`getForWorkspace``string $workspaceGid`, `array $optFields = []``PaginatedResponse`List tags in a workspace`create``array $data``TagData`Create a tag`createForWorkspace``string $workspaceGid`, `array $data``TagData`Create a tag in a workspace`update``string $gid`, `array $data``TagData`Update a tag`delete``string $gid``bool`Delete a tag```
// List tags in a workspace
$tags = Asana::tags()->getForWorkspace('workspace_gid');

// List tags on a task
$tags = Asana::tags()->getForTask('task_gid');

// Create a tag
$tag = Asana::tags()->create([
    'name' => 'Priority',
    'workspace' => 'workspace_gid',
    'color' => 'red',
]);

// Or create directly in a workspace
$tag = Asana::tags()->createForWorkspace('workspace_gid', [
    'name' => 'Urgent',
    'color' => 'hot-pink',
]);

// Update a tag
Asana::tags()->update('tag_gid', ['name' => 'High Priority']);

// Delete a tag
Asana::tags()->delete('tag_gid');
```

#### TagData Properties

[](#tagdata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"tag"``name``?string`Tag name`color``?string`Color (`"dark-pink"`, `"dark-green"`, `"red"`, etc.)`notes``?string`Description`created_at``?string`Creation timestamp`followers``?array`Followers`workspace``?CompactResource`Workspace`permalink_url``?string`URL to the tag in Asana---

### Stories (Comments)

[](#stories-comments)

Access via `Asana::stories()` — returns `StoryResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``StoryData`Get a story`getForTask``string $taskGid`, `array $optFields = []``PaginatedResponse`List stories on a task`create``string $taskGid`, `array $data``StoryData`Add a comment to a task`update``string $gid`, `array $data``StoryData`Update a comment`delete``string $gid``bool`Delete a comment```
// List comments/activity on a task
$stories = Asana::stories()->getForTask('task_gid');
foreach ($stories->data as $story) {
    echo "{$story->created_by->name}: {$story->text}\n";
}

// Add a comment
$story = Asana::stories()->create('task_gid', [
    'text' => 'This looks good, shipping it!',
]);

// Add a rich-text comment
$story = Asana::stories()->create('task_gid', [
    'html_text' => 'Done! See results.',
]);

// Pin a comment
Asana::stories()->update('story_gid', ['is_pinned' => true]);

// Delete a comment
Asana::stories()->delete('story_gid');
```

#### StoryData Properties

[](#storydata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"story"``text``?string`Plain-text content`html_text``?string`HTML content`type``?string``"comment"` or `"system"``resource_subtype``?string`Specific story type`created_at``?string`Creation timestamp`created_by``?CompactResource`Author`target``?CompactResource`Target resource (task, project, etc.)`is_pinned``?bool`Whether the story is pinned`is_edited``?bool`Whether the story was edited`sticker_name``?string`Sticker name if applicable---

### Attachments

[](#attachments)

Access via `Asana::attachments()` — returns `AttachmentResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``AttachmentData`Get an attachment`getForTask``string $taskGid`, `array $optFields = []``PaginatedResponse`List attachments on a task`create``string $parentGid`, `array $data``AttachmentData`Create an attachment`delete``string $gid``bool`Delete an attachment```
// List attachments on a task
$attachments = Asana::attachments()->getForTask('task_gid');

// Get attachment details (includes download URL)
$attachment = Asana::attachments()->get('attachment_gid');
echo $attachment->download_url;
echo $attachment->name;
echo $attachment->size; // bytes

// Create an external attachment
$attachment = Asana::attachments()->create('task_gid', [
    'resource_subtype' => 'external',
    'name' => 'Design Spec',
    'url' => 'https://example.com/spec.pdf',
]);

// Delete an attachment
Asana::attachments()->delete('attachment_gid');
```

#### AttachmentData Properties

[](#attachmentdata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"attachment"``name``?string`File name`resource_subtype``?string``"asana"`, `"dropbox"`, `"gdrive"`, `"onedrive"`, `"box"`, `"vimeo"`, or `"external"``created_at``?string`Creation timestamp`download_url``?string`URL to download the file (temporary)`host``?string`Service hosting the attachment`parent``?CompactResource`Parent task`permanent_url``?string`Permanent link`size``?int`File size in bytes`view_url``?string`URL to view in browser---

### Custom Fields

[](#custom-fields)

Access via `Asana::customFields()` — returns `CustomFieldResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``CustomFieldData`Get a custom field`getForWorkspace``string $workspaceGid`, `array $optFields = []``PaginatedResponse`List custom fields in a workspace`create``array $data``CustomFieldData`Create a custom field`update``string $gid`, `array $data``CustomFieldData`Update a custom field`delete``string $gid``bool`Delete a custom field```
// List custom fields in a workspace
$fields = Asana::customFields()->getForWorkspace('workspace_gid');

// Get a custom field
$field = Asana::customFields()->get('custom_field_gid');
echo $field->name;
echo $field->type; // "text", "number", "enum", "multi_enum", "date", "people"

// Create a number field
$field = Asana::customFields()->create([
    'name' => 'Story Points',
    'resource_subtype' => 'number',
    'workspace' => 'workspace_gid',
    'precision' => 0,
]);

// Create an enum field
$field = Asana::customFields()->create([
    'name' => 'Priority',
    'resource_subtype' => 'enum',
    'workspace' => 'workspace_gid',
    'enum_options' => [
        ['name' => 'Low', 'color' => 'green'],
        ['name' => 'Medium', 'color' => 'yellow'],
        ['name' => 'High', 'color' => 'red'],
    ],
]);

// Update a custom field
Asana::customFields()->update('field_gid', ['name' => 'Effort Points']);

// Delete a custom field
Asana::customFields()->delete('field_gid');
```

#### CustomFieldData Properties

[](#customfielddata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"custom_field"``name``?string`Field name`resource_subtype``?string``"text"`, `"number"`, `"enum"`, `"multi_enum"`, `"date"`, or `"people"``type``?string`Field type (deprecated, use `resource_subtype`)`description``?string`Field description`enabled``?bool`Whether the field is enabled`enum_options``?array`Options for enum fields`precision``?int`Decimal precision for number fields`format``?string`Display format (`"none"`, `"currency"`, `"custom"`, `"percentage"`)`currency_code``?string`ISO 4217 currency code`custom_label``?string`Custom label text`custom_label_position``?string``"prefix"` or `"suffix"``is_global_to_workspace``?bool`Available across the workspace`has_notifications_enabled``?bool`Notifications on change---

### Portfolios

[](#portfolios)

Access via `Asana::portfolios()` — returns `PortfolioResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``PortfolioData`Get a portfolio`list``string $workspaceGid`, `string $ownerGid`, `array $optFields = []``PaginatedResponse`List portfolios`getItems``string $portfolioGid`, `array $optFields = []``PaginatedResponse`List items in a portfolio`addItem``string $portfolioGid`, `string $itemGid``bool`Add a project to a portfolio`removeItem``string $portfolioGid`, `string $itemGid``bool`Remove a project from a portfolio`create``array $data``PortfolioData`Create a portfolio`update``string $gid`, `array $data``PortfolioData`Update a portfolio`delete``string $gid``bool`Delete a portfolio```
// List portfolios owned by a user
$portfolios = Asana::portfolios()->list('workspace_gid', 'owner_gid');

// Get a portfolio
$portfolio = Asana::portfolios()->get('portfolio_gid');

// Create a portfolio
$portfolio = Asana::portfolios()->create([
    'name' => 'Q1 Projects',
    'workspace' => 'workspace_gid',
    'color' => 'light-green',
]);

// Manage items (projects) in a portfolio
$items = Asana::portfolios()->getItems('portfolio_gid');
Asana::portfolios()->addItem('portfolio_gid', 'project_gid');
Asana::portfolios()->removeItem('portfolio_gid', 'project_gid');

// Update a portfolio
Asana::portfolios()->update('portfolio_gid', ['name' => 'Q1 Projects (Final)']);

// Delete a portfolio
Asana::portfolios()->delete('portfolio_gid');
```

#### PortfolioData Properties

[](#portfoliodata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"portfolio"``name``?string`Portfolio name`color``?string`Color`created_at``?string`Creation timestamp`created_by``?CompactResource`Creator`due_on``?string`Due date`start_on``?string`Start date`owner``?CompactResource`Owner`workspace``?CompactResource`Workspace`permalink_url``?string`URL to the portfolio in Asana`public``?bool`Whether visible to workspace`members``?array`Portfolio members`custom_fields``?array`Custom field values`custom_field_settings``?array`Custom field settings---

### Goals

[](#goals)

Access via `Asana::goals()` — returns `GoalResource`.

MethodParametersReturnsDescription`get``string $gid`, `array $optFields = []``GoalData`Get a goal`list``array $params = []``PaginatedResponse`List goals (filter by `workspace`, `team`, `project`, etc.)`create``array $data``GoalData`Create a goal`update``string $gid`, `array $data``GoalData`Update a goal`delete``string $gid``bool`Delete a goal`getSubgoals``string $goalGid``PaginatedResponse`List subgoals`addSubgoal``string $goalGid`, `string $subgoalGid``bool`Add a subgoal`getRelationships``string $goalGid``PaginatedResponse`List supporting work (projects/portfolios)`updateMetric``string $goalGid`, `array $data``GoalData`Update the goal's progress metric```
// List goals in a workspace
$goals = Asana::goals()->list(['workspace' => 'workspace_gid']);

// List goals for a team
$goals = Asana::goals()->list([
    'workspace' => 'workspace_gid',
    'team' => 'team_gid',
]);

// Get a goal
$goal = Asana::goals()->get('goal_gid');

// Create a goal
$goal = Asana::goals()->create([
    'name' => 'Ship v2.0',
    'workspace' => 'workspace_gid',
    'due_on' => '2025-06-30',
    'notes' => 'Release the next major version',
]);

// Manage subgoals
$subgoals = Asana::goals()->getSubgoals('goal_gid');
Asana::goals()->addSubgoal('goal_gid', 'subgoal_gid');

// Update progress metric
Asana::goals()->updateMetric('goal_gid', [
    'current_number_value' => 75,
]);

// Get supporting work
$relationships = Asana::goals()->getRelationships('goal_gid');

// Delete a goal
Asana::goals()->delete('goal_gid');
```

#### GoalData Properties

[](#goaldata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"goal"``name``?string`Goal name`owner``?CompactResource`Goal owner`due_on``?string`Due date`start_on``?string`Start date`html_notes``?string`HTML notes`notes``?string`Plain-text notes`status``?string``"green"`, `"yellow"`, `"red"`, or `"missed"``is_workspace_level``?bool`Workspace-level goal`liked``?bool`Whether you liked it`likes``?array`Users who liked`metric``?array`Progress metric config and values`team``?CompactResource`Team`workspace``?CompactResource`Workspace`followers``?array`Goal followers`num_likes``?int`Number of likes---

### Webhooks

[](#webhooks)

Access via `Asana::webhooks()` — returns `WebhookResource`.

MethodParametersReturnsDescription`get``string $gid``WebhookData`Get a webhook`getForWorkspace``string $workspaceGid`, `?string $resourceGid = null``PaginatedResponse`List webhooks (optionally filter by resource)`create``array $data``WebhookData`Create a webhook`update``string $gid`, `array $data``WebhookData`Update a webhook`delete``string $gid``bool`Delete a webhook```
// List all webhooks in a workspace
$webhooks = Asana::webhooks()->getForWorkspace('workspace_gid');

// List webhooks for a specific resource
$webhooks = Asana::webhooks()->getForWorkspace('workspace_gid', 'project_gid');

// Create a webhook
$webhook = Asana::webhooks()->create([
    'resource' => 'project_gid',
    'target' => 'https://your-app.com/asana/webhook',
    'filters' => [
        ['resource_type' => 'task', 'action' => 'changed', 'fields' => ['completed']],
    ],
]);

// Update webhook filters
Asana::webhooks()->update('webhook_gid', [
    'filters' => [
        ['resource_type' => 'task', 'action' => 'added'],
        ['resource_type' => 'task', 'action' => 'removed'],
    ],
]);

// Delete a webhook
Asana::webhooks()->delete('webhook_gid');
```

#### WebhookData Properties

[](#webhookdata-properties)

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Always `"webhook"``active``?bool`Whether the webhook is active`resource``?CompactResource`Watched resource`target``?string`Delivery target URL`created_at``?string`Creation timestamp`last_failure_at``?string`Last failure timestamp`last_failure_content``?string`Last failure details`last_success_at``?string`Last success timestamp`filters``?array`Event filters---

### Batch Requests

[](#batch-requests)

Access via `Asana::batch()` — returns `BatchResource`.

Send up to 10 API requests in a single HTTP call. Each action specifies a `relative_path`, `method`, and optionally `data` or `options`.

MethodParametersReturnsDescription`submit``array $actions``array`Submit batch of actions (max 10)Each action is an array with:

- `relative_path` (string) — API path, e.g. `/tasks/123`
- `method` (string) — `GET`, `POST`, `PUT`, `DELETE`
- `data` (array, optional) — Request body for POST/PUT
- `options` (array, optional) — Query params like `opt_fields`

Each result in the response contains its own `status_code` and `body`.

```
// Fetch multiple tasks at once
$results = Asana::batch()->submit([
    ['relative_path' => '/tasks/task_gid_1', 'method' => 'GET'],
    ['relative_path' => '/tasks/task_gid_2', 'method' => 'GET'],
    ['relative_path' => '/tasks/task_gid_3', 'method' => 'GET'],
]);
// $results[0]['status_code'] => 200
// $results[0]['body']['data'] => { task data }

// Update multiple tasks at once
$results = Asana::batch()->submit([
    [
        'relative_path' => '/tasks/task_1',
        'method' => 'PUT',
        'data' => ['completed' => true],
    ],
    [
        'relative_path' => '/tasks/task_2',
        'method' => 'PUT',
        'data' => ['completed' => true],
    ],
]);

// Mix different operations
$results = Asana::batch()->submit([
    ['relative_path' => '/users/me', 'method' => 'GET'],
    ['relative_path' => '/tasks/task_gid', 'method' => 'GET', 'options' => ['opt_fields' => 'name,completed']],
    ['relative_path' => '/tasks', 'method' => 'POST', 'data' => ['name' => 'New task', 'workspace' => 'ws_gid']],
]);
```

---

### Error Handling

[](#error-handling)

All API errors throw typed exceptions. Exceptions bubble up like any PHP exception — handle them with try-catch at whatever level makes sense (controller, service, or global handler).

ExceptionHTTP StatusExtra Methods`ValidationException`400`getErrors(): array``AuthenticationException`401—`ForbiddenException`403—`NotFoundException`404—`RateLimitException`429`getRetryAfter(): int``AsanaException`any other—All exceptions extend `AsanaException` and provide:

- `getMessage()` — error message from Asana
- `getCode()` — HTTP status code
- `getResponseBody()` — full response array

```
use WMBH\Asana\Exceptions\AsanaException;
use WMBH\Asana\Exceptions\NotFoundException;
use WMBH\Asana\Exceptions\RateLimitException;
use WMBH\Asana\Exceptions\ValidationException;

try {
    $task = Asana::tasks()->get('invalid_gid');
} catch (NotFoundException $e) {
    // 404 - task doesn't exist
    Log::warning("Task not found: {$e->getMessage()}");
} catch (RateLimitException $e) {
    // 429 - retry after N seconds
    $retryAfter = $e->getRetryAfter();
    Log::info("Rate limited, retry after {$retryAfter}s");
} catch (ValidationException $e) {
    // 400 - invalid data
    $errors = $e->getErrors();
    // [['message' => 'workspace: Missing input', 'help' => '...']]
} catch (AsanaException $e) {
    // Catch-all for any other API error
    Log::error("Asana error: {$e->getMessage()}", $e->getResponseBody());
}
```

You can also handle exceptions globally in your exception handler:

```
// app/Exceptions/Handler.php (or bootstrap/app.php in Laravel 11+)
$exceptions->render(function (RateLimitException $e) {
    return response()->json(['error' => 'Rate limited'], 429);
});
```

---

### Pagination

[](#pagination)

Methods that return collections use `PaginatedResponse`:

```
$page = Asana::tasks()->getForProject('project_gid', limit: 25);

// Access data
$tasks = $page->data; // TaskData[]

// Check for more pages
if ($page->hasNextPage()) {
    $nextPage = Asana::tasks()->getForProject(
        'project_gid',
        offset: $page->nextPageToken,
        limit: 25,
    );
}

// Iterate all pages
$allTasks = [];
$offset = null;

do {
    $page = Asana::tasks()->getForProject('project_gid', offset: $offset, limit: 100);
    $allTasks = array_merge($allTasks, $page->data);
    $offset = $page->nextPageToken;
} while ($page->hasNextPage());
```

#### PaginatedResponse Properties

[](#paginatedresponse-properties)

PropertyTypeDescription`data``array`Array of typed DTOs`nextPageToken``?string`Offset token for next page`nextPageUri``?string`Full URI for next page#### CompactResource (nested references)

[](#compactresource-nested-references)

Many DTOs contain nested references to other objects (assignee, workspace, project, etc.). These are represented as `CompactResource` with three properties:

PropertyTypeDescription`gid``string`Globally unique identifier`resource_type``?string`Type of the resource`name``?string`Name of the resource```
$task = Asana::tasks()->get('task_gid');
echo $task->assignee->gid;   // "12345"
echo $task->assignee->name;  // "Jane Doe"
echo $task->workspace->name; // "My Workspace"
```

---

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [WMBH](https://github.com/WMBH)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance84

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor2

2 contributors hold 50%+ of commits

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

83d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/06a643473c90d95f4d707e0e6c461a6e9eeeb1a9769055932fea6a14fa9b0737?d=identicon)[WMBH](/maintainers/WMBH)

---

Top Contributors

[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")[![WMBH](https://avatars.githubusercontent.com/u/51792289?v=4)](https://github.com/WMBH "WMBH (1 commits)")

---

Tags

apiclientlaravelasanawmbh

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/wmbh-laravel-asana/health.svg)

```
[![Health](https://phpackages.com/badges/wmbh-laravel-asana/health.svg)](https://phpackages.com/packages/wmbh-laravel-asana)
```

###  Alternatives

[codebar-ag/laravel-docuware

DocuWare integration with Laravel

1221.1k](/packages/codebar-ag-laravel-docuware)[codebar-ag/laravel-zammad

Zammad integration with Laravel

106.1k](/packages/codebar-ag-laravel-zammad)[njoguamos/laravel-plausible

A laravel package for interacting with plausible analytics api.

208.8k](/packages/njoguamos-laravel-plausible)[xelon-ag/vmware-php-client

PHP API Client for VmWare

114.3k](/packages/xelon-ag-vmware-php-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
