Guide

Create Asana Tasks from Email (CLI)

Client requests and bug reports arrive by email, but the work gets tracked in Asana. Most automation tools that bridge the two bill for every task they create. The CLI hands you each message as JSON; the Asana API turns it into a task in a chosen project. This guide builds an email-to-Asana pipeline you control, mapping subject and sender to task fields and running it on a schedule for free.

Written by Caleb Geene Director, Site Reliability Engineering

Reviewed by Qasim Muhammad

VerifiedCLI 3.1.17 · Gmail, Outlook · last tested June 9, 2026

Command references used in this guide: nylas email search, nylas email list, and nylas email read.

How do I create an Asana task from email?

Creating an Asana task from email takes two calls: pull the message as JSON, then POST it to Asana. The CLI returns structured messages with nylas email search --json, and a single request to the Asana API create task endpoint turns each one into a task in a project you pick. The whole flow is 2 commands, with no SMTP, no OAuth app registration, and no per-task fee.

First, get a token and a project ID. Asana issues a personal access token from the developer console, and you pass it as a bearer credential in the Authorization header per the token docs. The 2 inputs you need are that token and the numeric project GID from a project's URL. If the CLI isn't installed yet, run the Homebrew one-liner below, then see getting started for the other three install methods.

# Install the CLI (macOS / Linux)
brew install nylas/nylas-cli/nylas

# Pull the messages you want to file into Asana
nylas email search "*" --subject "bug" --after 2026-06-08 --json --limit 50 > items.json

How do I map an email to Asana task fields?

Map each email to an Asana task by sending a JSON body where name holds the subject and notes holds the sender and body. Asana wraps every request in a top-level data object, and a task needs a workspace or at least one project. Build the body with jq so values escape correctly and a missing subject never produces a nameless task.

The loop below reads one message at a time, extracts the subject and sender, and POSTs a task to the chosen project. Using jq -n to assemble the payload keeps the JSON valid even when a subject contains quotes or newlines — a common cause of a 400 response from the Asana API. The projects field takes an array of GIDs, so one task can land in several projects at once.

PROJECT_GID="1201234567890123"
jq -c '.[]' items.json | while read -r msg; do
  subject=$(echo "$msg" | jq -r '.subject // "(no subject)"')
  sender=$(echo "$msg"  | jq -r '.from[0].email // "unknown"')
  body=$(jq -n --arg n "$subject" --arg notes "From: $sender" --arg p "$PROJECT_GID" \
    '{data: {name: $n, notes: $notes, projects: [$p]}}')
  curl -s -X POST "https://app.asana.com/api/1.0/tasks" \
    -H "Authorization: Bearer $ASANA_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$body"
done

What Asana fields can I set from an email?

Beyond the name, the Asana create task endpoint accepts due_on, assignee, notes, and html_notes in the same data object. Due dates use a plain YYYY-MM-DD string for due_on, per the create task reference. The assignee field takes a user GID or the literal me.

That lets you route by sender. A message from your on-call alias gets a due date 1 day out and lands on you; everything else keeps the project default. The snippet picks a due date from the current date plus 1 day and assigns the task with me. Asana's html_notes field accepts a restricted HTML subset documented in the rich text reference, so you can keep links from the email body clickable.

subject=$(echo "$msg" | jq -r '.subject // "(no subject)"')
sender=$(echo "$msg"  | jq -r '.from[0].email // "unknown"')
due=$(date -u -d "+1 day" +%Y-%m-%d 2>/dev/null || date -u -v+1d +%Y-%m-%d)
jq -n --arg n "$subject" --arg notes "From: $sender" \
  --arg due "$due" --arg p "$PROJECT_GID" \
  '{data: {name: $n, notes: $notes, due_on: $due, assignee: "me", projects: [$p]}}'

How do I avoid creating duplicate tasks?

Avoid duplicate Asana tasks by recording each email's message ID after you file it and skipping IDs you have already seen. Every Nylas message carries a stable id field, so a flat file of processed IDs is enough to make the pipeline safe to run every 5 minutes from cron without ever double-filing a message.

The loop checks a seen.txt file before each POST and appends the ID after a successful create. Scoping the search to newer_than:1d caps the working set, and the ID check guards the edge where the same message matches two consecutive runs. Combined, the two limits keep a 1-minute cron job idempotent and stay well under Asana's default rate limit of 150 requests per minute.

touch seen.txt
jq -c '.[]' items.json | while read -r msg; do
  id=$(echo "$msg" | jq -r '.id')
  grep -qF "$id" seen.txt && continue
  subject=$(echo "$msg" | jq -r '.subject // "(no subject)"')
  curl -s -X POST "https://app.asana.com/api/1.0/tasks" \
    -H "Authorization: Bearer $ASANA_TOKEN" -H "Content-Type: application/json" \
    -d "$(jq -n --arg n "$subject" --arg p "$PROJECT_GID" '{data: {name: $n, projects: [$p]}}')" \
    && echo "$id" >> seen.txt
done

Why use a webhook instead of polling?

A webhook files an Asana task the instant an email arrives, instead of waiting for the next poll. The CLI ships a webhook server; run it with --tunnel cloudflared to expose a temporary public URL, so you can develop against live message.created events without deploying anything. A 5-minute poll adds up to 300 seconds of latency; a webhook fires within seconds of delivery.

The nylas webhook create command registers the endpoint and the triggers; nylas webhook server runs a local listener on port 9000 for testing. The mapping code from earlier is identical — only the trigger changes from a cron timer to an inbound event. Your Asana token stays separate from the mailbox grant the tool manages, so you rotate each one independently.

# Register a webhook for new mail
nylas webhook create \
  --url https://your-endpoint.example.com/hooks \
  --triggers message.created \
  --description "Email to Asana"

# Or test locally with an auto-generated tunnel
nylas webhook server --port 9000

Next steps